diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/action-gate.sh b/AINative_OKR_CASAN5/.specify/scripts/bash/action-gate.sh new file mode 100755 index 0000000..287e411 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/action-gate.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +set -uo pipefail + +# CASAN Track C-MVP — Tool authorization / ACTION gating (C1, V17). +# +# tool-registry-gate.sh authorizes by tool NAME + agent. This complements it by +# inspecting the ACTION itself — the command to run and the files it would +# write — because a legitimately-named tool can still attempt an illegitimate +# action (overwrite .env, curl|bash, rm -rf /, exfiltrate to an unknown host). +# +# Outcome model (per Plan-07 C0): ALLOW | WARN | REQUIRE_APPROVAL | BLOCK. +# BLOCK sensitive-file write, destructive/remote-exec command -> exit 2 +# REQUIRE_APPROVAL network egress, dependency install -> exit 3 +# (becomes ALLOW+audit when CASAN_ACTION_APPROVER is set) +# WARN noteworthy but permitted -> exit 0 (logged) +# ALLOW ordinary action -> exit 0 +# +# Usage: +# action-gate.sh --command "" [--write ...] +# action-gate.sh -- [args...] +# +# BLOCK is never overridable. REQUIRE_APPROVAL clears only with an explicit +# approver identity (audited) — approval is never silent. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +LOG="$PROJECT_ROOT/.specify/logs/level5/action-gate.jsonl" +mkdir -p "$(dirname "$LOG")" +# shellcheck source=casan-log.sh +source "$SCRIPT_DIR/casan-log.sh" + +CMD="" +WRITES=() +while [[ "$#" -gt 0 ]]; do + case "$1" in + --command) CMD="${2:-}"; shift 2 ;; + --write) WRITES+=("${2:-}"); shift 2 ;; + --) shift; CMD="$*"; break ;; + *) CMD="${CMD:+$CMD }$1"; shift ;; + esac +done + +if [[ -z "$CMD" && "${#WRITES[@]}" -eq 0 ]]; then + echo "Usage: action-gate.sh --command \"\" [--write ...] | -- " >&2 + exit 64 +fi + +APPROVER="${CASAN_ACTION_APPROVER:-}" + +RESULT="$(CASAN_AG_CMD="$CMD" CASAN_AG_WRITES="$(printf '%s\n' "${WRITES[@]:-}")" python - <<'PY' +import os, re + +cmd = os.environ.get("CASAN_AG_CMD", "") +writes = [w for w in os.environ.get("CASAN_AG_WRITES", "").splitlines() if w] +low = cmd.lower() + +# Sensitive-write targets (BLOCK). Checked against both explicit --write paths +# and any path-looking token in the command. +SENSITIVE = [ + r"(^|/)\.env(\.[a-z]+)?$", r"\.pem$", r"\.key$", r"(^|/)id_rsa$", r"(^|/)id_ed25519$", + r"\.p12$", r"\.pfx$", r"(^|/)\.ssh/", r"(^|/)\.github/workflows/", r"(^|/)\.git/hooks/", + r"(^|/)\.aws/credentials", r"(^|/)\.npmrc$", r"(^|/)\.pypirc$", r"(^|/)\.netrc$", + r"^/etc/", r"(^|/)\.dockercfg", r"(^|/)docker/config\.json$", +] +# Destructive / remote-exec commands (BLOCK). +DANGEROUS = [ + r"\brm\s+-rf?\s+(/|~|\$home|/\*|\.\s*$|\.\s|\*)", r":\(\)\s*\{\s*:\s*\|\s*:", r"\bmkfs\b", + r"\bdd\b[^\n]*of=/dev/", r"\bchmod\s+(-r\s+)?777\b", r">\s*/dev/sd", r"\bgit\s+push\b[^\n]*(--force|\s-f\b)", + r"(curl|wget)\b[^\n]*\|\s*(sudo\s+)?(ba)?sh\b", r"\bshred\b\s+/", r"\bchown\s+-r\s+root", +] +# Dependency install (REQUIRE_APPROVAL) — imperative form; C2 covers manifest diffs. +DEP_INSTALL = [ + r"\bnpm\s+(install|i|add)\s+\S", r"\byarn\s+add\s+\S", r"\bpnpm\s+add\s+\S", + r"\bpip3?\s+install\s+\S", r"\bpoetry\s+add\s+\S", r"\bgem\s+install\s+\S", + r"\bgo\s+get\s+\S", r"\bcargo\s+add\s+\S", r"\b(apt|apt-get|apk|brew|dnf|yum)\s+install\s+\S", +] +# Network egress (REQUIRE_APPROVAL) unless clearly local. +EGRESS = [r"\bcurl\b", r"\bwget\b", r"\bnc\b", r"\bncat\b", r"\bscp\b", r"\brsync\b[^\n]*::", r"\bssh\b\s+\S"] +LOCAL_OK = [r"127\.0\.0\.1", r"localhost", r"0\.0\.0\.0", r"::1", r"/api/tags"] + +def any_match(pats, text): + return next((p for p in pats if re.search(p, text)), None) + +# 1. Sensitive writes -> BLOCK +for path in writes + re.findall(r"[\w./~-]+", cmd): + m = any_match(SENSITIVE, path.lower()) + if m: + print(f"BLOCK|sensitive_file_write:{path}") + raise SystemExit(0) + +# 2. Destructive / remote-exec -> BLOCK +m = any_match(DANGEROUS, low) +if m: + print(f"BLOCK|dangerous_command:{m}") + raise SystemExit(0) + +# 3. Dependency install -> REQUIRE_APPROVAL +m = any_match(DEP_INSTALL, low) +if m: + print(f"REQUIRE_APPROVAL|dependency_install:{m}") + raise SystemExit(0) + +# 4. Network egress -> REQUIRE_APPROVAL unless clearly local +if any_match(EGRESS, low) and not any_match(LOCAL_OK, low): + print("REQUIRE_APPROVAL|network_egress") + raise SystemExit(0) + +# 5. sudo (non-destructive) -> WARN +if re.search(r"\bsudo\b", low): + print("WARN|privilege_escalation") + raise SystemExit(0) + +print("ALLOW|ok") +PY +)" + +OUTCOME="${RESULT%%|*}" +REASON="${RESULT#*|}" +TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + +# Approval clears REQUIRE_APPROVAL only with an explicit (audited) approver. +EFFECTIVE="$OUTCOME" +if [[ "$OUTCOME" == "REQUIRE_APPROVAL" && -n "$APPROVER" ]]; then + EFFECTIVE="ALLOW_APPROVED" +fi + +python - "$LOG" "$TS" "$OUTCOME" "$REASON" "$EFFECTIVE" "$APPROVER" "$CMD" <<'PY' +import json, sys +log, ts, outcome, reason, eff, approver, cmd = sys.argv[1:] +with open(log, "a", encoding="utf-8") as f: + f.write(json.dumps({ + "timestamp": ts, "harness": "C1-action-gate", "outcome": outcome, + "effective": eff, "reason": reason, "approver": approver, "command": cmd[:400], + }) + "\n") +PY + +case "$EFFECTIVE" in + BLOCK) + casan_log error action-gate "ACTION_BLOCKED reason=$REASON" + echo "ACTION_GATE outcome=BLOCK reason=$REASON" >&2 + exit 2 ;; + REQUIRE_APPROVAL) + casan_log warn action-gate "ACTION_REQUIRES_APPROVAL reason=$REASON (set CASAN_ACTION_APPROVER= to approve)" + echo "ACTION_GATE outcome=REQUIRE_APPROVAL reason=$REASON" >&2 + exit 3 ;; + ALLOW_APPROVED) + echo "ACTION_GATE outcome=ALLOW reason=approved_by:$APPROVER ($REASON)" + exit 0 ;; + WARN) + casan_log warn action-gate "ACTION_WARN reason=$REASON" + echo "ACTION_GATE outcome=WARN reason=$REASON" + exit 0 ;; + *) + echo "ACTION_GATE outcome=ALLOW reason=$REASON" + exit 0 ;; +esac diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-gate.sh b/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-gate.sh new file mode 100755 index 0000000..20ce4a6 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-gate.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -uo pipefail + +# CASAN Track C-MVP — Supply-chain gate for generated dependencies (C2, V18). +# +# An agent that writes code can also add a dependency. This gate diffs a +# dependency manifest against a baseline and decides: +# BLOCK malicious/denylisted package, typosquat of a known package, +# or a dangerous lifecycle script (postinstall/preinstall/install) -> exit 2 +# REQUIRE_APPROVAL a genuinely new dependency was added -> exit 3 +# (ALLOW+audit when CASAN_ACTION_APPROVER is set) +# ALLOW no new dependencies -> exit 0 +# +# Supports: package.json, requirements.txt, pom.xml, build.gradle(.kts). +# When npm audit / pip-audit / osv-scanner are installed they are recorded as +# available; otherwise the local denylist (malicious-packages.txt) is authoritative. +# The diff/scan logic lives in supply-chain-scan.py (deterministic, no network). +# +# Usage: supply-chain-gate.sh [baseline-manifest] [report.json] +# If baseline is omitted, the manifest's committed git version is used. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SEC_DIR="$PROJECT_ROOT/.specify/security" +# shellcheck source=casan-log.sh +source "$SCRIPT_DIR/casan-log.sh" + +MANIFEST="${1:-}" +BASELINE="${2:-}" +REPORT="${3:-$PROJECT_ROOT/.specify/logs/level5/supply-chain-report.json}" +mkdir -p "$(dirname "$REPORT")" + +if [[ -z "$MANIFEST" || ! -f "$MANIFEST" ]]; then + echo "Usage: supply-chain-gate.sh [baseline-manifest] [report.json]" >&2 + exit 64 +fi + +# Resolve baseline: explicit file, else the manifest's committed git version, +# else empty (treat every dependency as new). +BASELINE_TMP="" +if [[ -z "$BASELINE" ]]; then + BASELINE_TMP="$(mktemp)" + REL="${MANIFEST#"$PROJECT_ROOT"/}" + if git -C "$PROJECT_ROOT" show "HEAD:$REL" > "$BASELINE_TMP" 2>/dev/null; then + BASELINE="$BASELINE_TMP" + else + : > "$BASELINE_TMP"; BASELINE="$BASELINE_TMP" + fi +fi + +# Record which live scanners are available (honest capability reporting). +SCANNERS="" +for pair in "npm:npm" "pip-audit:pip-audit" "osv-scanner:osv-scanner"; do + command -v "${pair##*:}" >/dev/null 2>&1 && SCANNERS="${SCANNERS:+$SCANNERS }${pair%%:*}" +done + +RESULT="$(python "$SCRIPT_DIR/supply-chain-scan.py" \ + "$MANIFEST" "$BASELINE" "$SEC_DIR/known-packages.txt" "$SEC_DIR/malicious-packages.txt" \ + "$REPORT" "$SCANNERS")" +RC=$? +[[ -n "$BASELINE_TMP" ]] && rm -f "$BASELINE_TMP" +[[ "$RC" -ne 0 ]] && { echo "SUPPLY_CHAIN_SCAN_ERROR rc=$RC" >&2; exit 2; } + +OUTCOME="${RESULT%%|*}" +REASON="${RESULT#*|}" +APPROVER="${CASAN_ACTION_APPROVER:-}" + +case "$OUTCOME" in + BLOCK) + casan_log error supply-chain "SUPPLY_CHAIN_BLOCKED $REASON" + echo "SUPPLY_CHAIN_BLOCKED reason=$REASON report=$REPORT" >&2 + exit 2 ;; + REQUIRE_APPROVAL) + if [[ -n "$APPROVER" ]]; then + echo "SUPPLY_CHAIN_APPROVED by=$APPROVER new=$REASON report=$REPORT" + exit 0 + fi + casan_log warn supply-chain "SUPPLY_CHAIN_REQUIRES_APPROVAL new=$REASON (set CASAN_ACTION_APPROVER=)" + echo "SUPPLY_CHAIN_REQUIRES_APPROVAL new=$REASON report=$REPORT" >&2 + exit 3 ;; + *) + echo "SUPPLY_CHAIN_CLEAN reason=$REASON report=$REPORT" + exit 0 ;; +esac diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-scan.py b/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-scan.py new file mode 100755 index 0000000..19a1441 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/supply-chain-scan.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""CASAN Track C-MVP — supply-chain manifest diff + risk scan (C2, V18 core). + +Diffs a dependency manifest against a baseline and classifies the change. +Prints "|" on stdout and writes a JSON report. + +Argv: + + +Outcomes: BLOCK (denylist / typosquat / dangerous lifecycle), + REQUIRE_APPROVAL (new dependency added), ALLOW (no new deps). +Deterministic: no network; a local denylist is authoritative when no live +scanner is available (their availability is recorded honestly in the report). +""" +import json +import os +import re +import sys + + +def read(path: str) -> str: + try: + return open(path, encoding="utf-8").read() + except OSError: + return "" + + +def load_list(path: str): + out = [] + for line in read(path).splitlines(): + line = line.strip() + if line and not line.startswith("#"): + out.append(line) + return out + + +def parse_deps(text: str, name: str): + """Return ({pkg: version}, [dangerous lifecycle scripts]).""" + deps, scripts = {}, [] + base = os.path.basename(name).lower() + if base == "package.json" or name.endswith(".json"): + try: + data = json.loads(text) if text.strip() else {} + except ValueError: + data = {} + for sect in ("dependencies", "devDependencies", + "optionalDependencies", "peerDependencies"): + for k, v in (data.get(sect) or {}).items(): + deps[k] = str(v) + for hook in ("preinstall", "install", "postinstall"): + s = (data.get("scripts") or {}).get(hook) + if s: + scripts.append(hook + ":" + s) + elif base == "requirements.txt" or name.endswith(".txt"): + for line in text.splitlines(): + line = line.split("#", 1)[0].strip() + if not line: + continue + m = re.match(r"^([A-Za-z0-9._-]+)\s*([=<>!~].*)?$", line) + if m: + deps[m.group(1)] = (m.group(2) or "").strip() + elif base == "pom.xml" or name.endswith(".xml"): + for m in re.finditer(r"([^<]+)", text): + deps[m.group(1).strip()] = "" + elif base.startswith("build.gradle"): + for m in re.finditer(r"""['"]([\w.\-]+):([\w.\-]+):([\w.\-]+)['"]""", text): + deps[m.group(1) + ":" + m.group(2)] = m.group(3) + return deps, scripts + + +def levenshtein(a: str, b: str) -> int: + if abs(len(a) - len(b)) > 1: + return 2 + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, 1): + row = [i] + for j, cb in enumerate(b, 1): + row.append(min(prev[j] + 1, row[-1] + 1, prev[j - 1] + (ca != cb))) + prev = row + return prev[-1] + + +def main() -> int: + manifest, baseline, known_path, deny_path, report_path, scanners_raw = sys.argv[1:7] + scanners = [s for s in scanners_raw.split() if s] + known = load_list(known_path) + deny = set(load_list(deny_path)) + + cur, cur_scripts = parse_deps(read(manifest), manifest) + base_deps, base_scripts = parse_deps(read(baseline), manifest) + + added = {k: v for k, v in cur.items() if k not in base_deps} + new_scripts = [s for s in cur_scripts if s not in base_scripts] + + findings = [] + approval = [] + for name, ver in added.items(): + clean_ver = ver.lstrip("=<>!~^ ") + ident = (name + "@" + clean_ver) if ver else name + denied = ( + name in deny + or ident in deny + or any(d.split("@")[0] == name and "@" in d and d.split("@", 1)[1] in ver for d in deny) + ) + if denied: + findings.append({"package": name, "reason": "denylisted_or_known_malicious"}) + continue + if name not in known: + near = next((k for k in known if levenshtein(name.lower(), k.lower()) == 1), None) + if near: + findings.append({"package": name, "reason": "typosquat_of:" + near}) + continue + approval.append({"package": name, "version": ver}) + + for s in new_scripts: + findings.append({"package": "", "reason": "dangerous_lifecycle:" + s[:120]}) + + if findings: + outcome = "BLOCK" + elif approval: + outcome = "REQUIRE_APPROVAL" + else: + outcome = "ALLOW" + + report = { + "manifest": manifest, + "scanners_available": scanners, + "scanners_note": "local denylist authoritative when no live scanner present", + "added": [{"package": k, "version": v} for k, v in added.items()], + "new_lifecycle_scripts": new_scripts, + "blocked": findings, + "require_approval": approval, + "outcome": outcome, + } + with open(report_path, "w", encoding="utf-8") as f: + json.dump(report, f, indent=2, ensure_ascii=False) + + if findings: + reason = ";".join(x["package"] + ":" + x["reason"] for x in findings) + elif approval: + reason = ",".join(x["package"] for x in approval) + else: + reason = "no_new_dependencies" + print(outcome + "|" + reason) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/AINative_OKR_CASAN5/.specify/security/known-packages.txt b/AINative_OKR_CASAN5/.specify/security/known-packages.txt new file mode 100644 index 0000000..e4ec2fb --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/security/known-packages.txt @@ -0,0 +1,33 @@ +# Popular package names used for typosquat proximity checks (edit distance <= 1 +# to one of these, but not an exact match, is flagged as a likely typosquat). +# Extend as the project's real dependency surface grows. +express +react +react-dom +lodash +axios +vite +tailwindcss +zod +prisma +@prisma/client +bcrypt +jsonwebtoken +class-validator +class-transformer +@nestjs/core +@nestjs/common +@nestjs/jwt +@nestjs/swagger +@tanstack/react-query +react-router-dom +react-hook-form +requests +flask +django +numpy +pandas +pytest +requests-oauthlib +pyyaml +cryptography diff --git a/AINative_OKR_CASAN5/.specify/security/malicious-packages.txt b/AINative_OKR_CASAN5/.specify/security/malicious-packages.txt new file mode 100644 index 0000000..9f0bd55 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/security/malicious-packages.txt @@ -0,0 +1,17 @@ +# Known-bad / denylisted package identifiers. Format: one entry per line, either +# a bare name (any version) or name@version for a specific pinned bad release. +# This is a LOCAL denylist used when no live CVE/OSV scanner is available; a real +# deployment should also run npm audit / pip-audit / osv-scanner (the gate runs +# them when present and records tool availability in the report). +# +# The entries below are illustrative fixtures (documented malware families / +# typosquat campaigns) so the gate has deterministic denials to test against. +event-stream@3.3.6 +flatmap-stream +coa@2.0.3 +rc@1.2.9 +ua-parser-js@0.7.29 +node-ipc@10.1.1 +colors@1.4.44-liberty-2 +crossenv +cross-env.js