Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion anchor/core/constitution.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

# SHA-256 of the official legacy files (optional in V3).
CONSTITUTION_SHA256 = "46539F9D069801C3ACA161696E9AF264299F8B578A510FEA3A28318A58ADC136"
MITIGATION_SHA256 = "15554DB48B46C43816FA39B7E40F61F49975022058E6F1E264F2E2344B33409F"
MITIGATION_SHA256 = "80489726A05A31933EA1448FA1E4AFB6E27A8B628E40EFD73B99535E1283DA32"


# =============================================================================
Expand Down
24 changes: 23 additions & 1 deletion anchor/governance/mitigation.anchor
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand All @@ -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"
9 changes: 8 additions & 1 deletion anchor/runtime/decision_auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/test_feedback_fixes.py
Original file line number Diff line number Diff line change
@@ -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
Loading