-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add SecOps security agent with rule engine, audit log, and ToolInvoker integration #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
b39dcb4
9bbdcd9
fe78e7e
7e50440
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Gitleaks configuration — allowlist test files containing deliberately | ||
| # fake credentials used by the security rule detector test suite. | ||
|
|
||
| [allowlist] | ||
| paths = [ | ||
| '''tests/unit/security/rules/test_credential_detector\.py''', | ||
| '''tests/unit/security/test_output_scanner\.py''', | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,4 +37,5 @@ def default_config_dict() -> dict[str, Any]: | |
| "api": {}, | ||
| "sandboxing": {}, | ||
| "mcp": {}, | ||
| "security": {}, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -369,18 +369,42 @@ class CoordinationTopology(StrEnum): | |
|
|
||
|
|
||
| class ActionType(StrEnum): | ||
| """Convenience constants for common approval action types. | ||
| """Two-level action type taxonomy for security classification. | ||
|
|
||
| Models typically use ``NotBlankStr`` for ``action_type`` fields, so these | ||
| are optional helper constants and custom string values remain valid. | ||
| Used by autonomy presets (DESIGN_SPEC §12.2), SecOps validation | ||
| (§12.3), tiered timeout policies (§12.4), and progressive trust | ||
| (§11.3). Values follow a ``category:action`` naming convention. | ||
|
|
||
| Custom action type strings are also accepted by models that use | ||
| ``str`` for ``action_type`` fields — these enum members are | ||
| convenience constants for the built-in taxonomy. | ||
| """ | ||
|
|
||
| CODE_MERGE = "code_merge" | ||
| DEPLOYMENT = "deployment" | ||
| BUDGET_SPEND = "budget_spend" | ||
| EXTERNAL_COMMUNICATION = "external_communication" | ||
| HIRING = "hiring" | ||
| ARCHITECTURE_CHANGE = "architecture_change" | ||
| CODE_READ = "code:read" | ||
| CODE_WRITE = "code:write" | ||
| CODE_CREATE = "code:create" | ||
| CODE_DELETE = "code:delete" | ||
| CODE_REFACTOR = "code:refactor" | ||
| TEST_WRITE = "test:write" | ||
| TEST_RUN = "test:run" | ||
| DOCS_WRITE = "docs:write" | ||
| VCS_COMMIT = "vcs:commit" | ||
| VCS_PUSH = "vcs:push" | ||
| VCS_BRANCH = "vcs:branch" | ||
| DEPLOY_STAGING = "deploy:staging" | ||
| DEPLOY_PRODUCTION = "deploy:production" | ||
| COMMS_INTERNAL = "comms:internal" | ||
| COMMS_EXTERNAL = "comms:external" | ||
| BUDGET_SPEND = "budget:spend" | ||
| BUDGET_EXCEED = "budget:exceed" | ||
| ORG_HIRE = "org:hire" | ||
| ORG_FIRE = "org:fire" | ||
| ORG_PROMOTE = "org:promote" | ||
| VCS_READ = "vcs:read" | ||
| DB_QUERY = "db:query" | ||
| DB_MUTATE = "db:mutate" | ||
| DB_ADMIN = "db:admin" | ||
| ARCH_DECIDE = "arch:decide" | ||
|
Comment on lines
371
to
+407
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a migration path for legacy
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| class MergeOrder(StrEnum): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,12 +50,29 @@ | |
| EXECUTION_ENGINE_TIMEOUT, | ||
| EXECUTION_RECOVERY_FAILED, | ||
| ) | ||
| from ai_company.observability.events.security import SECURITY_DISABLED | ||
| from ai_company.providers.enums import MessageRole | ||
| from ai_company.providers.models import ChatMessage | ||
| from ai_company.security.audit import AuditLog | ||
| from ai_company.security.output_scanner import OutputScanner | ||
| from ai_company.security.rules.credential_detector import CredentialDetector | ||
| from ai_company.security.rules.data_leak_detector import DataLeakDetector | ||
| from ai_company.security.rules.destructive_op_detector import ( | ||
| DestructiveOpDetector, | ||
| ) | ||
| from ai_company.security.rules.engine import RuleEngine | ||
| from ai_company.security.rules.path_traversal_detector import ( | ||
| PathTraversalDetector, | ||
| ) | ||
| from ai_company.security.rules.policy_validator import PolicyValidator | ||
| from ai_company.security.rules.protocol import SecurityRule # noqa: TC001 | ||
| from ai_company.security.rules.risk_classifier import RiskClassifier | ||
| from ai_company.security.service import SecOpsService | ||
| from ai_company.tools.invoker import ToolInvoker | ||
| from ai_company.tools.permissions import ToolPermissionChecker | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ai_company.api.approval_store import ApprovalStore | ||
| from ai_company.budget.coordination_config import ErrorTaxonomyConfig | ||
| from ai_company.budget.enforcer import BudgetEnforcer | ||
| from ai_company.budget.tracker import CostTracker | ||
|
|
@@ -68,6 +85,8 @@ | |
| ) | ||
| from ai_company.providers.models import CompletionConfig | ||
| from ai_company.providers.protocol import CompletionProvider | ||
| from ai_company.security.config import SecurityConfig | ||
| from ai_company.security.protocol import SecurityInterceptionStrategy | ||
| from ai_company.tools.registry import ToolRegistry | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
@@ -108,6 +127,8 @@ def __init__( # noqa: PLR0913 | |
| shutdown_checker: ShutdownChecker | None = None, | ||
| error_taxonomy_config: ErrorTaxonomyConfig | None = None, | ||
| budget_enforcer: BudgetEnforcer | None = None, | ||
| security_config: SecurityConfig | None = None, | ||
| approval_store: ApprovalStore | None = None, | ||
| ) -> None: | ||
| self._provider = provider | ||
| self._loop: ExecutionLoop = execution_loop or ReactLoop() | ||
|
|
@@ -127,6 +148,8 @@ def __init__( # noqa: PLR0913 | |
| self._cost_tracker = budget_enforcer.cost_tracker | ||
| else: | ||
| self._cost_tracker = cost_tracker | ||
| self._security_config = security_config | ||
| self._approval_store = approval_store | ||
| self._recovery_strategy = recovery_strategy | ||
| self._shutdown_checker = shutdown_checker | ||
| self._error_taxonomy_config = error_taxonomy_config | ||
|
|
@@ -185,7 +208,7 @@ async def run( # noqa: PLR0913 | |
| await self._budget_enforcer.check_can_execute(agent_id) | ||
| identity = await self._budget_enforcer.resolve_model(identity) | ||
|
|
||
| tool_invoker = self._make_tool_invoker(identity) | ||
| tool_invoker = self._make_tool_invoker(identity, task_id=task_id) | ||
| ctx, system_prompt = self._prepare_context( | ||
| identity=identity, | ||
| task=task, | ||
|
|
@@ -642,15 +665,69 @@ async def _apply_recovery( | |
| ) | ||
| return execution_result | ||
|
|
||
| def _make_security_interceptor( | ||
| self, | ||
| ) -> SecurityInterceptionStrategy | None: | ||
| """Build the SecOps security interceptor if configured.""" | ||
| if self._security_config is None: | ||
| logger.warning( | ||
| SECURITY_DISABLED, | ||
| note="No SecurityConfig provided — all security checks skipped", | ||
| ) | ||
| return None | ||
|
Comment on lines
+673
to
+678
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail closed when Returning 🔒 Minimal fail-closed change if self._security_config is None:
- logger.warning(
+ logger.error(
SECURITY_DISABLED,
- note="No SecurityConfig provided — all security checks skipped",
+ note="SecurityConfig is required when tool execution is enabled",
)
- return None
+ msg = "SecurityConfig is required when tool execution is enabled"
+ raise ValueError(msg)🤖 Prompt for AI Agents |
||
| if not self._security_config.enabled: | ||
| return None | ||
|
|
||
| cfg = self._security_config | ||
| re_cfg = cfg.rule_engine | ||
| policy_validator = PolicyValidator( | ||
| hard_deny_action_types=frozenset(cfg.hard_deny_action_types), | ||
| auto_approve_action_types=frozenset(cfg.auto_approve_action_types), | ||
| ) | ||
| # Build the detector list respecting config flags. | ||
| detectors: list[SecurityRule] = [policy_validator] | ||
| if re_cfg.credential_patterns_enabled: | ||
| detectors.append(CredentialDetector()) | ||
| if re_cfg.path_traversal_detection_enabled: | ||
| detectors.append(PathTraversalDetector()) | ||
| if re_cfg.destructive_op_detection_enabled: | ||
| detectors.append(DestructiveOpDetector()) | ||
| if re_cfg.data_leak_detection_enabled: | ||
| detectors.append(DataLeakDetector()) | ||
|
|
||
|
Comment on lines
+684
to
+698
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This factory only installs 🤖 Prompt for AI Agents |
||
| rule_engine = RuleEngine( | ||
| rules=tuple(detectors), | ||
| risk_classifier=RiskClassifier(), | ||
| config=re_cfg, | ||
| ) | ||
| return SecOpsService( | ||
| config=cfg, | ||
| rule_engine=rule_engine, | ||
| audit_log=AuditLog(), | ||
| output_scanner=OutputScanner(), | ||
| approval_store=self._approval_store, | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
greptile-apps[bot] marked this conversation as resolved.
|
||
|
|
||
| def _make_tool_invoker( | ||
| self, | ||
| identity: AgentIdentity, | ||
| task_id: str | None = None, | ||
| ) -> ToolInvoker | None: | ||
| """Create a ToolInvoker with permission checking, or None.""" | ||
| """Create a ToolInvoker with permission checking and security. | ||
|
|
||
| Returns None if no tool registry is configured. | ||
| """ | ||
| if self._tool_registry is None: | ||
| return None | ||
| checker = ToolPermissionChecker.from_permissions(identity.tools) | ||
| return ToolInvoker(self._tool_registry, permission_checker=checker) | ||
| interceptor = self._make_security_interceptor() | ||
| return ToolInvoker( | ||
| self._tool_registry, | ||
| permission_checker=checker, | ||
| security_interceptor=interceptor, | ||
| agent_id=str(identity.id), | ||
| task_id=task_id, | ||
| ) | ||
|
Comment on lines
712
to
+731
|
||
|
|
||
| def _log_completion( | ||
| self, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Security event constants.""" | ||
|
|
||
| from typing import Final | ||
|
|
||
| SECURITY_EVALUATE_START: Final[str] = "security.evaluate.start" | ||
| SECURITY_EVALUATE_COMPLETE: Final[str] = "security.evaluate.complete" | ||
| SECURITY_RULE_MATCHED: Final[str] = "security.rule.matched" | ||
| SECURITY_RULE_ERROR: Final[str] = "security.rule.error" | ||
| SECURITY_VERDICT_ALLOW: Final[str] = "security.verdict.allow" | ||
| SECURITY_VERDICT_DENY: Final[str] = "security.verdict.deny" | ||
| SECURITY_VERDICT_ESCALATE: Final[str] = "security.verdict.escalate" | ||
| SECURITY_AUDIT_RECORDED: Final[str] = "security.audit.recorded" | ||
| SECURITY_AUDIT_EVICTION: Final[str] = "security.audit.eviction" | ||
| SECURITY_OUTPUT_SCAN_START: Final[str] = "security.output_scan.start" | ||
| SECURITY_OUTPUT_SCAN_FINDING: Final[str] = "security.output_scan.finding" | ||
| SECURITY_ESCALATION_CREATED: Final[str] = "security.escalation.created" | ||
| SECURITY_ESCALATION_STORE_ERROR: Final[str] = "security.escalation.store_error" | ||
| SECURITY_CONFIG_LOADED: Final[str] = "security.config.loaded" | ||
| SECURITY_DISABLED: Final[str] = "security.disabled" | ||
| SECURITY_RISK_FALLBACK: Final[str] = "security.risk.fallback" | ||
| SECURITY_CREDENTIAL_DETECTED: Final[str] = "security.credential.detected" | ||
| SECURITY_PATH_TRAVERSAL_DETECTED: Final[str] = "security.path_traversal.detected" | ||
| SECURITY_DESTRUCTIVE_OP_DETECTED: Final[str] = "security.destructive_op.detected" | ||
| SECURITY_DATA_LEAK_DETECTED: Final[str] = "security.data_leak.detected" | ||
| SECURITY_POLICY_DENY: Final[str] = "security.policy.deny" | ||
| SECURITY_POLICY_AUTO_APPROVE: Final[str] = "security.policy.auto_approve" | ||
| SECURITY_INTERCEPTOR_ERROR: Final[str] = "security.interceptor.error" | ||
| SECURITY_OUTPUT_SCAN_ERROR: Final[str] = "security.output_scan.error" | ||
| SECURITY_AUDIT_CONFIG_ERROR: Final[str] = "security.audit.config_error" | ||
| SECURITY_SCAN_DEPTH_EXCEEDED: Final[str] = "security.scan.depth_exceeded" | ||
| SECURITY_AUDIT_RECORD_ERROR: Final[str] = "security.audit.record_error" | ||
| SECURITY_ACTION_TYPE_INVALID: Final[str] = "security.action_type.invalid" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Security subsystem — SecOps agent, rule engine, audit, and models. | ||
|
|
||
| Public API: | ||
|
|
||
| - ``SecOpsService`` — the meta-agent coordinating security. | ||
| - ``SecurityConfig`` — top-level security configuration. | ||
| - ``SecurityVerdict`` / ``SecurityVerdictType`` — evaluation results. | ||
| - ``SecurityContext`` — tool invocation context for evaluation. | ||
| - ``AuditEntry`` / ``AuditLog`` — audit recording. | ||
| - ``OutputScanResult`` / ``OutputScanner`` — post-tool output scanning. | ||
| - ``SecurityInterceptionStrategy`` — protocol for the ToolInvoker. | ||
| - ``ActionTypeRegistry`` / ``ActionTypeCategory`` — action taxonomy. | ||
| - ``RuleEngine`` / ``SecurityRule`` — rule evaluation. | ||
| """ | ||
|
|
||
| from ai_company.security.action_types import ( | ||
| ActionTypeCategory, | ||
| ActionTypeRegistry, | ||
| ) | ||
| from ai_company.security.audit import AuditLog | ||
| from ai_company.security.config import ( | ||
| RuleEngineConfig, | ||
| SecurityConfig, | ||
| SecurityPolicyRule, | ||
| ) | ||
| from ai_company.security.models import ( | ||
| AuditEntry, | ||
| OutputScanResult, | ||
| SecurityContext, | ||
| SecurityVerdict, | ||
| SecurityVerdictType, | ||
| ) | ||
| from ai_company.security.output_scanner import OutputScanner | ||
| from ai_company.security.protocol import SecurityInterceptionStrategy | ||
| from ai_company.security.rules.engine import RuleEngine | ||
| from ai_company.security.rules.protocol import SecurityRule | ||
| from ai_company.security.service import SecOpsService | ||
|
|
||
| __all__ = [ | ||
| "ActionTypeCategory", | ||
| "ActionTypeRegistry", | ||
| "AuditEntry", | ||
| "AuditLog", | ||
| "OutputScanResult", | ||
| "OutputScanner", | ||
| "RuleEngine", | ||
| "RuleEngineConfig", | ||
| "SecOpsService", | ||
| "SecurityConfig", | ||
| "SecurityContext", | ||
| "SecurityInterceptionStrategy", | ||
| "SecurityPolicyRule", | ||
| "SecurityRule", | ||
| "SecurityVerdict", | ||
| "SecurityVerdictType", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What allowlist mechanisms does the current Gitleaks version support for suppressing only specific known test secrets—such as regex-scoped allowlists or inlinegitleaks:allowannotations—instead of excluding an entire file?💡 Result:
Current Gitleaks (v8.x) supports these fine‑grained allowlisting options to suppress specific known test secrets without excluding whole files:
#gitleaks:allow(or equivalent comment syntax in your language) on the same line as the test secret; Gitleaks will ignore that finding. There’s also a CLI flag--ignore-gitleaks-allowto disable honoring these inline allow comments. [1].gitleaksignorefile containing the finding Fingerprint value(s) to suppress only those exact findings. [1][[rules.allowlists]]): attach one or more allowlists to a specific rule; a finding is ignored if any allowlist matches. Each allowlist can match on commits, paths, regexes, and stopwords, with a configurablecondition(OR/AND) andregexTargetscoping (e.g., apply regexes to the extractedsecretvs the fullmatchvs the wholeline). [2][[allowlists]]): same match primitives (commits/paths/regexes/stopwords) andregexTargetscoping, applied at higher precedence than rule allowlists; additionally, you can scope a shared allowlist to only certain rules usingtargetRules(so it’s not truly global). [2]Sources: Gitleaks upstream README/config docs. [1][2]
References
gitleaks:allow,.gitleaksignore, and related flags: (github.com)regexTarget,condition,targetRules: (github.com)Citations:
🏁 Script executed:
Repository: Aureliolo/ai-company
Length of output: 264
🏁 Script executed:
Repository: Aureliolo/ai-company
Length of output: 443
🏁 Script executed:
Repository: Aureliolo/ai-company
Length of output: 6271
Use inline allowlisting for test credentials instead of blanket file exemptions.
Gitleaks v8.x supports narrower allowlisting mechanisms: add
#gitleaks:allowcomments on lines with intentional test credentials, or use.gitleaksignorewith finding fingerprints. File-wide path allowlisting will suppress any future real secrets accidentally added to these files.🤖 Prompt for AI Agents