AID-H-031.002: Static Admission Gates for AI-Generated Artifacts

Version 1.20261015 · Actionable sub-technique

Apply stricter static policy checks to AI-generated code, infrastructure-as-code, workflow files, dependency manifests, deployment definitions, and repository automation before they can be merged or built. These gates should check for dangerous constructs, unsafe sinks, secrets exposure, dependency hallucination or typosquatting, privileged path modification, insecure default configuration, and policy violations specific to 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.
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

Scan the exact provenance-bound Git diff for policy-defined sabotage signals and require signed security review on findings.
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

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-031.002: Static Admission Gates for AI-Generated Artifacts | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version