AID-H-031.002: Static Admission Gates for AI-Generated Artifacts
Pillars: infra, app
Lifecycle: building, validation
Scope boundary
Owns provenance-aware static-stage routing and policy for AI-generated development artifacts, including dependency hallucination, privileged-path escalation, insecure defaults, and required stage sufficiency. It emits a static-stage result but cannot authorize promotion.
AID-H-025.001: AID-H-031.002 applies AI-generation provenance and promotion policy around static findings; AID-H-025.001 performs generic dangerous-construct analysis on exact executable bytes. The generic scanner is one component of the AI-specific static admission stage.
AID-H-031.004: AID-H-031.002 emits the static-stage decision; AID-H-031.004 verifies all routed evidence and alone authorizes merge, build, or deployment. A static PASS cannot bypass required dynamic validation or approval.
Implementation guidance
- AID-H-031.002-G001: Verify signed, digest-bound scanner attestations for every routed check and fail closed on sensitive paths.
- AID-H-031.002-G002: Scan the exact provenance-bound Git diff for policy-defined sabotage signals and require signed security review on findings.
AID-H-031.002-G001
Concept:
The admission gate verifies signed provenance and routing decisions, rehashes the pinned Git change set, and then verifies one scanner-produced attestation for every required check. A caller-provided list of check names is never completion evidence. Sensitive paths require the review role and signed approval selected by routing.
# File: ci/verify_static_admission.py
from __future__ import annotations
import hashlib
import json
import os
import subprocess
import tempfile
COMMAND_TIMEOUT_SECONDS = int(os.environ["VERIFIED_COMMAND_TIMEOUT_SECONDS"])
if COMMAND_TIMEOUT_SECONDS <= 0:
raise RuntimeError("verified command timeout must be positive")
from pathlib import Path
from ci.build_generated_change_provenance import changed_blobs, derived_risks
CHECKS = {
"semgrep": {
"schema": "aidefend.static-check.semgrep.v1",
"identity": "https://github.com/example/security-ci/.github/workflows/semgrep.yml@refs/heads/main",
},
"gitleaks": {
"schema": "aidefend.static-check.gitleaks.v1",
"identity": "https://github.com/example/security-ci/.github/workflows/gitleaks.yml@refs/heads/main",
},
"trivy_config": {
"schema": "aidefend.static-check.trivy-config.v1",
"identity": "https://github.com/example/security-ci/.github/workflows/trivy.yml@refs/heads/main",
},
"dependency_resolution": {
"schema": "aidefend.static-check.dependencies.v1",
"identity": "https://github.com/example/security-ci/.github/workflows/dependencies.yml@refs/heads/main",
},
"coding_agent_sabotage": {
"schema": "aidefend.static-check.sabotage.v1",
"identity": "https://github.com/example/security-ci/.github/workflows/sabotage.yml@refs/heads/main",
},
}
OIDC_ISSUER = "https://token.actions.githubusercontent.com"
def strict_json_object(raw: bytes, label: str) -> dict:
def unique_pairs(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f"duplicate {label} key: {key}")
value[key] = item
return value
def reject_constant(value):
raise ValueError(f"non-finite {label} number: {value}")
document = json.loads(
raw.decode("utf-8", errors="strict"),
object_pairs_hook=unique_pairs, parse_constant=reject_constant,
)
if not isinstance(document, dict):
raise ValueError(f"{label} root must be an object")
return document
def verify_blob(path: Path, bundle: Path, identity: str) -> bytes:
raw = path.read_bytes()
snapshot: Path | None = None
try:
with tempfile.NamedTemporaryFile(
prefix="static-check-report-", suffix=".json", delete=False
) as handle:
handle.write(raw)
handle.flush()
os.fsync(handle.fileno())
snapshot = Path(handle.name)
subprocess.run([
"cosign", "verify-blob", "--bundle", str(bundle),
"--certificate-identity", identity,
"--certificate-oidc-issuer", OIDC_ISSUER, str(snapshot),
], check=True, capture_output=True, text=True, timeout=COMMAND_TIMEOUT_SECONDS)
return raw
finally:
if snapshot is not None:
snapshot.unlink(missing_ok=True)
def verify_check(
check_id: str,
evidence_dir: Path,
provenance: dict,
policy: dict,
) -> dict:
contract = CHECKS.get(check_id)
if contract is None:
raise PermissionError(f"unregistered required check: {check_id}")
report_path = evidence_dir / f"{check_id}.json"
bundle_path = evidence_dir / f"{check_id}.sigstore.json"
report_raw = verify_blob(report_path, bundle_path, contract["identity"])
report = strict_json_object(report_raw, f"{check_id} report")
if report.get("schema_version") != contract["schema"]:
raise ValueError(f"check schema differs: {check_id}")
expected = {
"check_id": check_id,
"base_commit": provenance["base_commit"],
"head_commit": provenance["head_commit"],
"change_set_sha256": provenance["change_set_sha256"],
"policy_sha256": policy["check_policy_sha256"][check_id],
}
if any(report.get(field) != value for field, value in expected.items()):
raise PermissionError(f"check binding differs: {check_id}")
if not report.get("tool_image_digest", "").startswith("sha256:"):
raise ValueError(f"check tool image is not digest-pinned: {check_id}")
if report.get("outcome") not in {"pass", "fail", "insufficient_data", "review_required"}:
raise ValueError(f"check outcome is invalid: {check_id}")
return report
def gate(
provenance: dict,
routing: dict,
policy: dict,
evidence_dir: Path,
approval: dict | None,
) -> dict:
recomputed = changed_blobs(provenance["base_commit"], provenance["head_commit"])
if recomputed != provenance["file_manifest"]:
raise PermissionError("Git change set differs from signed provenance")
risks = derived_risks(recomputed)
required = routing.get("required_checks")
if not isinstance(required, list) or required != sorted(set(required)) or not required:
raise ValueError("routing required_checks are invalid")
reports = [verify_check(check_id, evidence_dir, provenance, policy) for check_id in required]
outcomes = {item["check_id"]: item["outcome"] for item in reports}
if any(value == "insufficient_data" for value in outcomes.values()):
final = "insufficient_data"
elif any(value in {"fail", "review_required"} for value in outcomes.values()):
final = "fail"
else:
final = "pass"
if risks:
required_roles = set(routing.get("required_review_roles", []))
if "security_owner" not in required_roles:
raise PermissionError("sensitive change lacks required security-owner route")
if (
approval is None
or approval.get("schema_version") != "aidefend.security-approval.v1"
or approval.get("change_set_sha256") != provenance["change_set_sha256"]
or approval.get("decision") != "approve"
):
final = "fail"
return {
"schema_version": "aidefend.generated-static-admission.v2",
"outcome": final,
"provenance_sha256": hashlib.sha256(
json.dumps(provenance, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
"routing_sha256": hashlib.sha256(
json.dumps(routing, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
"change_set_sha256": provenance["change_set_sha256"],
"check_report_sha256": {
item["check_id"]: hashlib.sha256(
(evidence_dir / f"{item['check_id']}.json").read_bytes()
).hexdigest()
for item in reports
},
"check_outcomes": outcomes,
"derived_risk_paths": risks,
}
Action: Verify signatures on provenance, routing, check policy, every check report, and any security approval before calling the gate. This stage emits signed static-admission evidence only; AID-H-031.004 alone authorizes promotion. Missing expected checks, unavailable scanners, unpinned images, or incomplete bindings is insufficient_data and blocks merge.
Production implementation
At static admission, require signed scanner attestations bound to scanner/config/ruleset, artifact and policy digests, validate complete routed-check and file populations, deny missing/error/finding results according to policy and never accept a caller-asserted pass.
Independent verification
A separately credentialed verifier independently reads back or replays scanner attestations, raw outputs, routed matrix and file coverage by replaying seeded vulnerability, secret, dependency, policy, missing-file and scanner-error fixtures, runs positive, negative, boundary, stale-policy, partial-population and dependency-failure fixtures, and reconciles the complete eligible population without trusting a component-supplied verdict.
AID-H-031.002-G002
Concept:
Sabotage-pattern scanning is a review-blocking signal, not proof of malicious intent. It scans the exact base/head commits from verified provenance, uses a signed policy for terms and thresholds, and emits a change-set-bound report for the static admission gate.
# File: ci/check_coding_agent_sabotage.py
from __future__ import annotations
import json
import math
import os
import subprocess
from pathlib import Path
GIT_TIMEOUT_SECONDS = float(os.environ["VERIFIED_H031_SABOTAGE_GIT_TIMEOUT_SECONDS"])
GIT_OUTPUT_MAX_BYTES = int(os.environ["VERIFIED_H031_SABOTAGE_GIT_OUTPUT_MAX_BYTES"])
if not math.isfinite(GIT_TIMEOUT_SECONDS) or GIT_TIMEOUT_SECONDS <= 0 or GIT_OUTPUT_MAX_BYTES <= 0:
raise RuntimeError("verified sabotage-scan Git bounds are invalid")
def git(*args: str) -> str:
raw = subprocess.check_output(["git", *args], timeout=GIT_TIMEOUT_SECONDS)
if len(raw) > GIT_OUTPUT_MAX_BYTES:
raise RuntimeError("Git output exceeds the signed byte cap")
return raw.decode("utf-8", errors="strict")
def diff_for(base: str, head: str, path: str) -> str:
return git("diff", "--no-ext-diff", "--unified=0", base, head, "--", path)
def added_lines(diff: str) -> list[str]:
return [
line[1:] for line in diff.splitlines()
if line.startswith("+") and not line.startswith("+++")
]
def removed_test_signals(diff: str, policy: dict) -> int:
terms = tuple(value.lower() for value in policy["removed_test_terms"])
return sum(
1 for line in diff.splitlines()
if line.startswith("-") and any(term in line.lower() for term in terms)
)
def scan(provenance: dict, policy: dict) -> dict:
findings = []
for item in provenance["file_manifest"]:
path = item["path"]
diff = diff_for(provenance["base_commit"], provenance["head_commit"], path)
lowered = "\n".join(added_lines(diff)).lower()
hits = []
for category, terms in policy["added_diff_terms"].items():
for term in terms:
if str(term).lower() in lowered:
hits.append(f"{category}:{term}")
removed = removed_test_signals(diff, policy)
sensitive = any(path.startswith(prefix) for prefix in policy["sensitive_prefixes"])
if hits or removed >= int(policy["removed_test_review_threshold"]) or (
sensitive and item["status"] == "D"
):
findings.append({
"path": path,
"status": item["status"],
"term_hits": sorted(hits),
"removed_test_signals": removed,
"sensitive_file_deleted": bool(sensitive and item["status"] == "D"),
})
return {
"schema_version": "aidefend.static-check.sabotage.v1",
"check_id": "coding_agent_sabotage",
"base_commit": provenance["base_commit"],
"head_commit": provenance["head_commit"],
"change_set_sha256": provenance["change_set_sha256"],
"policy_sha256": policy["policy_sha256"],
"tool_image_digest": policy["tool_image_digest"],
"outcome": "review_required" if findings else "pass",
"findings": findings,
}
Evidence: The security scanner verifies the provenance and policy signatures, rehashes the Git blobs, writes canonical JSON, and signs the report under its dedicated workflow identity. The admission gate blocks auto-merge on review_required until a separately signed named security-owner approval binds the same change set.
Production implementation
At diff admission, parse the Git diff without executing content, scan policy-defined high-risk sinks and semantic patterns, retain line/path evidence, block high-confidence prohibited changes and require a signed qualified review bound to the unchanged diff for review-class findings.
Independent verification
A separately credentialed verifier independently reads back or replays diff parser, rule/semantic finding and reviewer binding with obfuscation, split-line, generated file, rename, binary, false-positive, post-review mutation and missing-review fixtures, runs positive, negative, boundary, stale-policy, partial-population and dependency-failure fixtures, and reconciles the complete eligible population without trusting a component-supplied verdict.
Open-source tools
- Semgrep
- Trivy
- Syft
- Grype
- Conftest
- Open Policy Agent (OPA)
- Gitleaks
- Checkov
Commercial / hosted tools
- GitHub Advanced Security
- Snyk Code
- Veracode
- Checkmarx One
Threat mappings
MITRE ATLAS
- AML.T0010 AI Supply Chain Compromise
- AML.T0011 User Execution
- AML.T0010.001 AI Supply Chain Compromise: AI Software
- AML.T0011.001 User Execution: Malicious Package
- AML.T0011.000 User Execution: Unsafe AI Artifacts
- AML.T0060 Publish Hallucinated Entities
- AML.T0095.000 Search Open Websites/Domains: Code Repositories (static gates catch secrets and risky repository automation)
MAESTRO
- Supply Chain Attacks (Cross-Layer)
- Compromised Framework Components (L3)
- Compromised Container Images (L4)
- Infrastructure-as-Code (IaC) Manipulation (L4)
OWASP LLM Top 10 2026
- LLM04:2026 Supply Chain
- LLM10:2026 Improper Output Handling
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
- ASI05:2026 Unexpected Code Execution (RCE)
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.2.1 Code Detection / Malicious Code Output (scans the exact generated-code artifact before release)
- AISubtech-2.3.2 Verifier Manipulation (detects changes that remove or weaken the generated-code verifier tests)
- AISubtech-9.3.1 Malicious Package / Tool Injection
- AISubtech-9.3.2 Dependency Name Squatting (Tools / Servers)
- AITech-12.2 Insecure Output Handling (scans the exact generated-code artifact before release)
- AITech-2.3 Reward Hacking (detects changes that remove or weaken the generated-code verifier tests)
- AITech-8.3 Information Disclosure
- AITech-9.3 Dependency / Plugin Compromise
Google Secure AI Framework 2.0 - Risks
- IMO: Insecure Model Output
- IIC: Insecure Integrated Component
Databricks AI Security Framework 3.0
- Model 7.3: ML Supply chain vulnerabilities
- Model 7.4: Source code control attack
- Platform 12.5: Poor security in the software development lifecycle
- Agents - Core 13.11: Unexpected RCE and Code Attacks