diff --git a/anchor/core/constitution.py b/anchor/core/constitution.py index bd2bdd2..59eab94 100644 --- a/anchor/core/constitution.py +++ b/anchor/core/constitution.py @@ -21,7 +21,7 @@ # SHA-256 of the official legacy files (optional in V3). CONSTITUTION_SHA256 = "46539F9D069801C3ACA161696E9AF264299F8B578A510FEA3A28318A58ADC136" -MITIGATION_SHA256 = "15554DB48B46C43816FA39B7E40F61F49975022058E6F1E264F2E2344B33409F" +MITIGATION_SHA256 = "80489726A05A31933EA1448FA1E4AFB6E27A8B628E40EFD73B99535E1283DA32" # ============================================================================= diff --git a/anchor/governance/mitigation.anchor b/anchor/governance/mitigation.anchor index b637827..ff5b1bb 100644 --- a/anchor/governance/mitigation.anchor +++ b/anchor/governance/mitigation.anchor @@ -82,7 +82,7 @@ mitigations: type: "regex" # Only fire on bulk access or sensitive key names pattern: >- - ^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ) + (?i)^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ) message: "Broad environment variable access detected. Agents may harvest secrets from env." severity: "error" # --- SEC-012: Governance Configuration Modification --- @@ -96,3 +96,25 @@ mitigations: \.(write|save|update)\s*\(.*\.anchor["'] message: "Unauthorized attempt to modify Anchor governance configuration detected. Governance state must be immutable at runtime." severity: "blocker" + + # --- SEC-004: Hardcoded Secrets (Mitigation Catalog Extension) --- + - id: "MIT-004-B" + rule_id: "SEC-004" + name: "Hardcoded API Key / Secret" + match: + type: "regex" + pattern: >- + (?i)(?:[a-zA-Z0-9_]*key|[a-zA-Z0-9_]*secret|[a-zA-Z0-9_]*token|password|credential)\s*=\s*['\"][a-zA-Z0-9_\-\.\:\/]{16,}['\"] + message: "Hardcoded API key, secret, token or credential detected." + severity: "error" + + # --- SEC-010: Insecure Python Execution --- + - id: "MIT-010-A" + rule_id: "SEC-010" + name: "Insecure Dynamic Python Execution Sinks" + match: + type: "regex" + pattern: >- + (?i)\b(?:eval|exec|pickle\.loads|pickle\.load|marshal\.loads|shelve\.open)\s*\( + message: "Dynamic python execution sink (eval/exec/pickle/marshal) detected." + severity: "error" diff --git a/anchor/runtime/decision_auditor.py b/anchor/runtime/decision_auditor.py index 2e4935c..e6b1ed9 100644 --- a/anchor/runtime/decision_auditor.py +++ b/anchor/runtime/decision_auditor.py @@ -261,7 +261,14 @@ def audit(self, provider: str, prompt: str, response: str, findings: list, juris timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") entry_id = str(uuid.uuid4()) rule_ids = sorted([f.get("rule_id") for f in all_violations if f.get("rule_id")]) - findings_hash = self._hash_payload(json.dumps(rule_ids)) + + # Salt the findings commitment to prevent offline dictionary enumeration + secret_key = os.environ.get("ANCHOR_MAT", os.environ.get("ANCHOR_SECRET_KEY", "default-key")).strip() + findings_hash = hmac.new( + secret_key.encode("utf-8"), + (json.dumps(rule_ids) + entry_id).encode("utf-8"), + hashlib.sha256 + ).hexdigest() prev_hash = self.get_last_runtime_hash() chain_hash = self._hash_payload(prev_hash + findings_hash) diff --git a/tests/unit/test_feedback_fixes.py b/tests/unit/test_feedback_fixes.py new file mode 100644 index 0000000..d48531e --- /dev/null +++ b/tests/unit/test_feedback_fixes.py @@ -0,0 +1,65 @@ +import re +import hmac +import hashlib +import json +from unittest.mock import patch, MagicMock +from anchor.runtime.decision_auditor import DecisionAuditor +from anchor.runtime.models import AuditEntry + +def test_sec_004_regex_compilation(): + # Verify that the SEC-004 regex pattern compiles and executes without errors in Python + # (Checking the fix for "global flags not at the start of the expression") + pattern = r"(?i)^(?:[^\"\'#]|([\"'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)" + + # Try compiling + compiled = re.compile(pattern) + assert compiled is not None + + # Try matching + match_str = "os.environ['API_KEY']" + assert compiled.search(match_str) is not None + +def test_findings_hash_salting(): + # Verify that findings_hash is computed using HMAC-SHA256 and salted with entry_id + # (So same rules with different entry_ids produce different findings_hash) + auditor = DecisionAuditor() + + # Mock runtime chain hash retrieval + auditor.get_last_runtime_hash = MagicMock(return_value="0".zfill(64)) + + # Run audit 1 + entry1 = auditor.audit( + provider="test-provider", + prompt="test prompt", + response="test response", + findings=[{"rule_id": "SEC-004"}] + ) + + # Run audit 2 with same rules/inputs + entry2 = auditor.audit( + provider="test-provider", + prompt="test prompt", + response="test response", + findings=[{"rule_id": "SEC-004"}] + ) + + # Verify entry_ids are different + assert entry1["entry_id"] != entry2["entry_id"] + + # Verify findings_hashes are different (due to entry_id salting) + assert entry1["cryptography"]["findings_hash"] != entry2["cryptography"]["findings_hash"] + +def test_static_detectors_mitigations(): + # Test MIT-004-B (Hardcoded API Key / Secret) regex matches + mit_004_b_pattern = r"(?i)(?:[a-zA-Z0-9_]*key|[a-zA-Z0-9_]*secret|[a-zA-Z0-9_]*token|password|credential)\s*=\s*['\"][a-zA-Z0-9_\-\.\:\/]{16,}['\"]" + rx_004 = re.compile(mit_004_b_pattern) + assert rx_004.search("openai_key = 'sk-proj-1234567890abcdef1234567890'") is not None + assert rx_004.search("secret_key = \"mysecretkey12345\"") is not None + assert rx_004.search("api-key = '12345'") is None # Too short (<16 chars) + + # Test MIT-010-A (Insecure Python Execution sinks) regex matches + mit_010_a_pattern = r"(?i)\b(?:eval|exec|pickle\.loads|pickle\.load|marshal\.loads|shelve\.open)\s*\(" + rx_010 = re.compile(mit_010_a_pattern) + assert rx_010.search("eval(user_code)") is not None + assert rx_010.search("pickle.loads(payload)") is not None + assert rx_010.search("other_func()") is None