security-mcp 1.3.1 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -887
- package/defaults/cloud-controls/aws.json +10712 -0
- package/defaults/cloud-controls/azure.json +7201 -0
- package/defaults/cloud-controls/gcp.json +4061 -0
- package/defaults/control-catalog.json +24 -0
- package/dist/ci/pr-gate.js +22 -5
- package/dist/cli/index.js +73 -2
- package/dist/cli/install.js +4 -55
- package/dist/cli/onboarding.js +18 -10
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +1 -1
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +1 -1
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +14 -3
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/k8s.js +841 -1
- package/dist/gate/checks/secrets.js +49 -37
- package/dist/gate/cloud-controls/apply.js +115 -0
- package/dist/gate/cloud-controls/bicep.js +36 -0
- package/dist/gate/cloud-controls/cfn.js +125 -0
- package/dist/gate/cloud-controls/detect.js +104 -0
- package/dist/gate/cloud-controls/hcl.js +140 -0
- package/dist/gate/cloud-controls/types.js +87 -0
- package/dist/gate/exceptions.js +78 -7
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +40 -3
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +9 -0
- package/dist/mcp/model-router.js +3 -3
- package/dist/mcp/orchestration.js +194 -41
- package/dist/mcp/server.js +124 -17
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +14 -1
- package/dist/review/store.js +4 -2
- package/dist/tests/run.js +124 -1
- package/package.json +6 -4
- package/skills/advanced-dos-tester/SKILL.md +9 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +9 -0
- package/skills/ai-llm-redteam/SKILL.md +9 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +9 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +9 -0
- package/skills/android-penetration-tester/SKILL.md +9 -0
- package/skills/anti-replay-tester/SKILL.md +9 -0
- package/skills/appsec-code-auditor/SKILL.md +9 -0
- package/skills/artifact-integrity-analyst/SKILL.md +9 -0
- package/skills/attack-navigator/SKILL.md +9 -0
- package/skills/auth-session-hacker/SKILL.md +9 -0
- package/skills/aws-penetration-tester/SKILL.md +54 -0
- package/skills/azure-penetration-tester/SKILL.md +52 -0
- package/skills/binary-auth-validator/SKILL.md +9 -0
- package/skills/bot-detection-specialist/SKILL.md +9 -0
- package/skills/business-logic-attacker/SKILL.md +9 -0
- package/skills/capec-code-mapper/SKILL.md +9 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +9 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +9 -0
- package/skills/ciso-orchestrator/SKILL.md +11 -0
- package/skills/cloud-infra-specialist/SKILL.md +9 -0
- package/skills/compliance-gap-analyst/SKILL.md +9 -0
- package/skills/compliance-grc/SKILL.md +9 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +9 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +9 -0
- package/skills/crypto-pki-specialist/SKILL.md +9 -0
- package/skills/csa-ccm-mapper/SKILL.md +9 -0
- package/skills/csf2-governance-mapper/SKILL.md +9 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +9 -0
- package/skills/dependency-confusion-attacker/SKILL.md +9 -0
- package/skills/device-integrity-aggregator/SKILL.md +9 -0
- package/skills/dos-resilience-tester/SKILL.md +9 -0
- package/skills/dread-scorer/SKILL.md +9 -0
- package/skills/egress-policy-enforcer/SKILL.md +9 -0
- package/skills/evidence-collector/SKILL.md +9 -0
- package/skills/file-upload-attacker/SKILL.md +9 -0
- package/skills/gcp-penetration-tester/SKILL.md +51 -0
- package/skills/git-history-secret-scanner/SKILL.md +9 -0
- package/skills/gitops-delivery-auditor/SKILL.md +120 -0
- package/skills/iac-security-auditor/SKILL.md +125 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +9 -0
- package/skills/incident-responder/SKILL.md +9 -0
- package/skills/injection-specialist/SKILL.md +9 -0
- package/skills/ios-security-auditor/SKILL.md +9 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +22 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +9 -0
- package/skills/kill-switch-engineer/SKILL.md +9 -0
- package/skills/linddun-privacy-analyst/SKILL.md +9 -0
- package/skills/logic-race-fuzzer/SKILL.md +9 -0
- package/skills/mobile-api-network-attacker/SKILL.md +9 -0
- package/skills/mobile-binary-hardener/SKILL.md +9 -0
- package/skills/mobile-security-specialist/SKILL.md +9 -0
- package/skills/mobile-webview-auditor/SKILL.md +9 -0
- package/skills/model-extraction-attacker/SKILL.md +9 -0
- package/skills/multipart-abuse-tester/SKILL.md +9 -0
- package/skills/oauth-pkce-specialist/SKILL.md +9 -0
- package/skills/parser-exhaustion-tester/SKILL.md +9 -0
- package/skills/pentest-infra/SKILL.md +9 -0
- package/skills/pentest-social/SKILL.md +9 -0
- package/skills/pentest-team/SKILL.md +9 -0
- package/skills/pentest-web-api/SKILL.md +9 -0
- package/skills/privacy-flow-analyst/SKILL.md +9 -0
- package/skills/prompt-injection-specialist/SKILL.md +9 -0
- package/skills/quantum-migration-planner/SKILL.md +9 -0
- package/skills/rag-poisoning-specialist/SKILL.md +9 -0
- package/skills/registry-mirror-enforcer/SKILL.md +9 -0
- package/skills/rotation-validation-agent/SKILL.md +9 -0
- package/skills/samm-assessor/SKILL.md +9 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +9 -0
- package/skills/senior-security-engineer/SKILL.md +11 -0
- package/skills/serialization-memory-attacker/SKILL.md +9 -0
- package/skills/session-timeout-tester/SKILL.md +9 -0
- package/skills/slsa-level3-enforcer/SKILL.md +9 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +9 -0
- package/skills/ssrf-detection-validator/SKILL.md +9 -0
- package/skills/step-up-auth-enforcer/SKILL.md +9 -0
- package/skills/stride-pasta-analyst/SKILL.md +9 -0
- package/skills/supply-chain-devsecops/SKILL.md +9 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +9 -0
- package/skills/threat-modeler/SKILL.md +9 -0
- package/skills/tls-certificate-auditor/SKILL.md +9 -0
- package/skills/token-reuse-detector/SKILL.md +9 -0
- package/skills/trike-risk-modeler/SKILL.md +9 -0
- package/skills/unicode-homograph-tester/SKILL.md +9 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +9 -0
- package/skills/webhook-security-tester/SKILL.md +9 -0
- package/skills/zero-trust-architect/SKILL.md +9 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
5
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// Bad-actor "Skills" / agentic-instruction threat detection.
|
|
7
|
+
//
|
|
8
|
+
// Scans the agentic-instruction surface of an AUDITED repository — the files an
|
|
9
|
+
// AI coding agent ingests as authority the moment it opens the repo:
|
|
10
|
+
// SKILL.md, AGENTS.md, CLAUDE.md, .claude/**, .cursorrules, .cursor/**,
|
|
11
|
+
// .windsurfrules, .github/copilot-instructions.md, .mcp.json
|
|
12
|
+
// A poisoned instruction file can hijack the agent (prompt injection), exfiltrate
|
|
13
|
+
// secrets, register destructive tools, or persist itself — entirely outside the
|
|
14
|
+
// application's own code. This is distinct from the framework's self-download
|
|
15
|
+
// sanitizer (src/mcp/orchestration.ts), which only protects skills the framework
|
|
16
|
+
// installs for itself.
|
|
17
|
+
//
|
|
18
|
+
// Maps to OWASP LLM01 (Prompt Injection), MITRE ATLAS AML.T0051 / AML.T0054,
|
|
19
|
+
// CWE-77 / CWE-94 / CWE-116.
|
|
20
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// File globs for the agentic-instruction surface. Deliberately does NOT ignore
|
|
22
|
+
// .claude/ (unlike repo/search.ts) — those files ARE the attack surface here.
|
|
23
|
+
const AGENTIC_GLOBS = [
|
|
24
|
+
"**/SKILL.md",
|
|
25
|
+
"**/AGENTS.md",
|
|
26
|
+
"**/CLAUDE.md",
|
|
27
|
+
"**/.claude/**/*.md",
|
|
28
|
+
"**/.claude/**/*.json",
|
|
29
|
+
"**/.cursorrules",
|
|
30
|
+
"**/.cursor/**/*.md",
|
|
31
|
+
"**/.cursor/**/*.mdc",
|
|
32
|
+
"**/.windsurfrules",
|
|
33
|
+
"**/.github/copilot-instructions.md",
|
|
34
|
+
"**/.mcp.json"
|
|
35
|
+
];
|
|
36
|
+
const AGENTIC_IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
|
|
37
|
+
// ─── AGENT_INSTRUCTION_OVERRIDE ──────────────────────────────────────────────
|
|
38
|
+
const OVERRIDE_IGNORE_RE = /ignore\s+(?:all\s+|any\s+)?(?:the\s+)?(?:previous|prior|above|preceding|earlier|foregoing)\s+(?:instructions|prompts?|context|rules|messages)/i;
|
|
39
|
+
const OVERRIDE_DISREGARD_RE = /disregard\s+(?:all\s+|any\s+)?(?:the\s+)?(?:previous|prior|above|earlier|foregoing|system)/i;
|
|
40
|
+
const OVERRIDE_TAGS_RE = /<\s*\/?\s*system\s*>|\[\s*\/?\s*system\s*\]|\[\/?INST\]|<\|im_start\|>|###\s*system/i;
|
|
41
|
+
const OVERRIDE_FORGET_RE = /(?:forget|disregard)\s+(?:everything|all\s+(?:previous|prior))|you\s+are\s+now\s+|new\s+instructions?\s*:|system\s+prompt\s+override/i;
|
|
42
|
+
const OVERRIDE_SAFETY_RE = /override\s+(?:the\s+)?(?:system|safety|previous|default)\s+(?:prompt|instructions?|guardrails?|rules)|do\s+not\s+(?:tell|inform|warn|mention\s+to)\s+the\s+user/i;
|
|
43
|
+
// ─── AGENT_INSTRUCTION_EXFIL ─────────────────────────────────────────────────
|
|
44
|
+
// Outbound fetch/curl directive to a non-allowlisted external host.
|
|
45
|
+
const EXFIL_FETCH_RE = /(?:fetch|curl|wget|axios(?:\.(?:get|post))?|requests\.(?:get|post)|http\.(?:get|post)|XMLHttpRequest|sendBeacon)\s*\(?\s*["'`]?https?:\/\/(?!raw\.githubusercontent\.com|github\.com|localhost|127\.0\.0\.1)/i;
|
|
46
|
+
const EXFIL_SEND_RE = /(?:send|post|upload|exfiltrat\w*|leak|transmit|beacon|ship)\b[^.\n]{0,48}(?:env(?:ironment)?\b|secret|token|api[_-]?key|credential|password|\.ssh|\.env\b|private\s+key)/i;
|
|
47
|
+
// ─── AGENT_TOOL_POISONING ────────────────────────────────────────────────────
|
|
48
|
+
const TOOL_DESTRUCTIVE_RE = /\brm\s+-rf\b|\bchmod\s+777\b|:\s*\(\s*\)\s*\{|\bmkfs\b|\bdd\s+if=|\beval\s*\(|\bexec(?:Sync)?\s*\(|child_process|os\.system|subprocess\.(?:call|run|Popen)|\bnc\s+-e|\/dev\/tcp\//i;
|
|
49
|
+
// Data-destruction / sabotage directives beyond raw shell exec.
|
|
50
|
+
const TOOL_SABOTAGE_RE = /git\s+push\s+(?:-f\b|--force)|git\s+reset\s+--hard|git\s+clean\s+-[a-z]*f|\bdrop\s+table\b|\btruncate\s+table\b|\bdelete\s+from\b(?!\s+\w+\s+where)|\bshred\b|>\s*\/dev\/sd[a-z]|\bformat\s+[a-z]:|\brimraf\b/i;
|
|
51
|
+
const TOOL_IMPERATIVE_DESC_RE = /"description"\s*:\s*"[^"]*(?:always\s+(?:run|execute|call|invoke)|before\s+(?:answering|responding|you\s+reply)|ignore\s+(?:the\s+)?(?:user|previous)|do\s+not\s+(?:tell|inform|reveal|mention))/i;
|
|
52
|
+
const TOOL_DISABLE_AUTH_RE = /(?:disable|turn\s+off|remove)\s+(?:the\s+)?(?:auth\w*|security|safety|guardrails?|validation|sandbox)|skip\s+(?:auth\w*|verification|approval|the\s+review)|bypass\s+(?:auth\w*|security|the\s+sandbox|review)/i;
|
|
53
|
+
// ─── AGENT_PERSISTENCE_DIRECTIVE ─────────────────────────────────────────────
|
|
54
|
+
const PERSIST_EVERY_RE = /on\s+every\s+(?:invocation|run|start|session|message|turn|request)/i;
|
|
55
|
+
const PERSIST_START_RE = /at\s+the\s+(?:start|beginning)\s+of\s+(?:every|each)\b/i;
|
|
56
|
+
const PERSIST_AUTOUPDATE_RE = /auto.?(?:update|reinstall|re-?install|download|fetch)\s+(?:this\s+)?(?:skill|agent|tool|file)|\bensure_skill\s*\(/i;
|
|
57
|
+
// ─── AGENT_HIDDEN_INSTRUCTION ────────────────────────────────────────────────
|
|
58
|
+
// Zero-width, bidi-override, and isolate characters used to smuggle instructions
|
|
59
|
+
// past human reviewers. CWE-116 / MITRE ATLAS AML.T0051.
|
|
60
|
+
const HIDDEN_INVISIBLE_RE = new RegExp("[\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u2069\\ufeff\\u00ad\\u2028\\u2029]");
|
|
61
|
+
const HIDDEN_HTML_COMMENT_RE = /<!--[\s\S]{0,200}?(?:ignore|system\s+prompt|instruction|execute|\bfetch\b|secret|password|do\s+not\s+(?:tell|mention))[\s\S]{0,200}?-->/i;
|
|
62
|
+
const HIDDEN_CSS_HIDE_RE = /(?:display\s*:\s*none|font-size\s*:\s*0|opacity\s*:\s*0|color\s*:\s*#?(?:fff(?:fff)?|white))/i;
|
|
63
|
+
const IMPERATIVE_WORD_RE = /\b(?:ignore|execute|run|fetch|send|delete|disable|override|reveal|exfiltrat\w*)\b/i;
|
|
64
|
+
const BASE64_BLOB_RE = /[A-Za-z0-9+/]{40,}={0,2}/;
|
|
65
|
+
const BASE64_DECODED_BAD_RE = /ignore|system\b|instruction|fetch\s*\(|secret|exec\s*\(|https?:\/\/|password/i;
|
|
66
|
+
// ─── AGENT_CREDENTIAL_HARVEST ────────────────────────────────────────────────
|
|
67
|
+
const CRED_READ_RE = /(?:read|cat\s|open|load|collect|dump|grab|access)\s+[^.\n]{0,32}(?:\.env\b|\.aws\/credentials|\.ssh\/|id_rsa|\.netrc|keychain|process\.env\b|credentials\.json)/i;
|
|
68
|
+
const CRED_EXFIL_RE = /(?:print|output|reveal|show|display|return|echo)\s+[^.\n]{0,32}(?:env(?:ironment)?\s+(?:vars?|variables)|all\s+secrets|api\s+keys|credentials|tokens)/i;
|
|
69
|
+
// ─── AGENT_MEMORY_POISONING ──────────────────────────────────────────────────
|
|
70
|
+
const MEM_FALSEPOS_RE = /(?:write|add|insert|save|record)\s+[^.\n]{0,48}false.?positive/i;
|
|
71
|
+
const MEM_SUPPRESS_RE = /(?:mark|record|suppress|whitelist|silence)\s+[^.\n]{0,36}(?:finding|alert|vulnerabilit\w+)[^.\n]{0,20}as\s+(?:safe|resolved|false|benign)/i;
|
|
72
|
+
const MEM_API_RE = /write_agent_memory[^.\n]{0,60}(?:false|suppress|ignore|whitelist)/i;
|
|
73
|
+
// ─── AGENT_JAILBREAK_PERSONA (folds into override) ───────────────────────────
|
|
74
|
+
// Roleplay / unrestricted-mode jailbreaks and non-English override phrasings.
|
|
75
|
+
const JAILBREAK_RE = /\bDAN\b|do\s+anything\s+now|developer\s+mode|jailbreak|unrestricted\s+mode|without\s+(?:any\s+)?(?:restrictions?|limitations?|filters?|guardrails?|rules)|no\s+(?:restrictions?|filters?|guardrails?|ethical\s+guidelines?)|act\s+as\s+if\s+you\s+have\s+no\s+(?:rules|restrictions|limits)|ignore\s+your\s+(?:safety\s+)?(?:guidelines|training|programming|policies)|pretend\s+(?:you\s+are|to\s+be)\s+(?:an?\s+)?(?:unrestricted|uncensored)/i;
|
|
76
|
+
const MULTILANG_OVERRIDE_RE = /忽略(?:以上|之前|前面|上述)|无视(?:以上|之前)|前の指示を無視|이전\s*지시\s*무시|ignora\s+(?:las\s+)?instrucciones\s+anteriores|ignorez\s+les\s+instructions\s+pr[eé]c[eé]dentes|ignoriere\s+(?:die\s+)?(?:vorherigen|vorigen)\s+anweisungen|игнорир\w*\s+предыдущие\s+инструкции|تجاهل\s+التعليمات\s+السابقة|ignore\s+as\s+instru[cç][õo]es\s+anteriores/i;
|
|
77
|
+
// ─── AGENT_REMOTE_INSTRUCTION_LOAD ───────────────────────────────────────────
|
|
78
|
+
// Pulling instructions/commands in from an external location (inbound payload).
|
|
79
|
+
const REMOTE_INSTR_RE = /(?:load|fetch|import|read|follow|execute|obey|retrieve)\s+[^.\n]{0,40}(?:instructions?|rules?|prompt|commands?|steps?|directives?|config\w*)[^.\n]{0,24}(?:from|at|located\s+at|hosted\s+at)\s+https?:\/\//i;
|
|
80
|
+
const CMD_SUBST_RE = /\$\(\s*(?:curl|wget|fetch)\b|`\s*(?:curl|wget)\b|<\(\s*curl|\beval\s+["'`]?\$\(/i;
|
|
81
|
+
// ─── AGENT_PERMISSION_ESCALATION ─────────────────────────────────────────────
|
|
82
|
+
const PERM_ESCALATION_RE = /--dangerously-skip-permissions|bypassPermissions|--yolo\b|auto[_-]?approve|allowed-tools[^.\n]{0,40}(?:Bash\s*\(\s*\*|\*\s*\)|:\s*\*|all\b)|add\s+[^.\n]{0,30}to\s+(?:the\s+)?allowed[_-]?tools|grant\s+(?:yourself|the\s+agent)\s+[^.\n]{0,20}(?:full|all|admin)\s+(?:access|permissions?)|run\s+[^.\n]{0,20}without\s+(?:asking|confirmation|approval|permission)/i;
|
|
83
|
+
// ─── AGENT_BACKDOOR_INSERT ───────────────────────────────────────────────────
|
|
84
|
+
const BACKDOOR_RE = /authorized_keys|add\s+(?:my\s+|this\s+|the\s+following\s+)?ssh\s+(?:public\s+)?key|create\s+(?:an?\s+)?(?:admin|root|superuser|backdoor)\s+(?:user|account)|reverse\s+shell|bind\s+shell|add\s+[^.\n]{0,30}(?:webhook|backdoor)|hardcode\s+[^.\n]{0,24}(?:token|password|api[_-]?key|secret)|insert\s+[^.\n]{0,24}backdoor|disable\s+[^.\n]{0,20}(?:2fa|mfa|signature\s+(?:check|verification))/i;
|
|
85
|
+
// ─── AGENT_PROMPT_LEAK (system-prompt / instruction extraction) ──────────────
|
|
86
|
+
const PROMPT_LEAK_RE = /(?:repeat|print|reveal|show|output|display|tell\s+me|reproduce|echo)\s+(?:back\s+)?(?:your|the|all\s+(?:your|the))\s+(?:system\s+|initial\s+|original\s+)?(?:prompt|instructions|rules|guidelines|configuration|directives)|what\s+(?:are|were)\s+your\s+(?:initial\s+|original\s+|exact\s+)?(?:instructions|rules|system\s+prompt)/i;
|
|
87
|
+
// ─── AGENT_INSTRUCTION_EXFIL — markdown image/link beacon ─────────────────────
|
|
88
|
+
const MD_BEACON_RE = /!?\[[^\]]*\]\(\s*https?:\/\/[^)]*[?&][^)=]*=\s*[^)]*\)/i;
|
|
89
|
+
// ─── AGENT_HIDDEN_INSTRUCTION — homoglyph / mixed-script confusables ──────────
|
|
90
|
+
// A token mixing Latin with Cyrillic/Greek letters (e.g. spoofed skill name).
|
|
91
|
+
const HOMOGLYPH_RE = /[A-Za-z][Ѐ-ӿͰ-Ͽ]|[Ѐ-ӿͰ-Ͽ][A-Za-z]/;
|
|
92
|
+
function makeAcc() {
|
|
93
|
+
return {
|
|
94
|
+
override: [], exfil: [], toolPoison: [], persist: [], hidden: [], cred: [], memory: [],
|
|
95
|
+
remoteLoad: [], permEsc: [], backdoor: [], promptLeak: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function isPrintableInstruction(decoded) {
|
|
99
|
+
if (!decoded || decoded.length < 6)
|
|
100
|
+
return false;
|
|
101
|
+
const printable = decoded.replace(/[^\x20-\x7e]/g, "").length / Math.max(decoded.length, 1);
|
|
102
|
+
return printable > 0.8 && BASE64_DECODED_BAD_RE.test(decoded);
|
|
103
|
+
}
|
|
104
|
+
function rot13(s) {
|
|
105
|
+
return s.replace(/[a-z]/gi, (c) => {
|
|
106
|
+
const base = c <= "Z" ? 65 : 97;
|
|
107
|
+
return String.fromCodePoint(((c.codePointAt(0) - base + 13) % 26) + base);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function tryBase64(text) {
|
|
111
|
+
const m = BASE64_BLOB_RE.exec(text);
|
|
112
|
+
if (!m)
|
|
113
|
+
return "";
|
|
114
|
+
try {
|
|
115
|
+
return Buffer.from(m[0], "base64").toString("utf-8");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function tryHex(text) {
|
|
122
|
+
const m = /(?:0x|\\x)?[0-9a-fA-F]{32,}/.exec(text);
|
|
123
|
+
if (!m)
|
|
124
|
+
return "";
|
|
125
|
+
const cleaned = m[0].replace(/0x|\\x/g, "");
|
|
126
|
+
if (cleaned.length % 2 !== 0)
|
|
127
|
+
return "";
|
|
128
|
+
try {
|
|
129
|
+
return Buffer.from(cleaned, "hex").toString("utf-8");
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function tryUnicodeEsc(text) {
|
|
136
|
+
if (!/\\u[0-9a-fA-F]{4}/.test(text))
|
|
137
|
+
return "";
|
|
138
|
+
return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCodePoint(Number.parseInt(h, 16)));
|
|
139
|
+
}
|
|
140
|
+
function tryPercent(text) {
|
|
141
|
+
if (!/%[0-9a-fA-F]{2}/.test(text))
|
|
142
|
+
return "";
|
|
143
|
+
try {
|
|
144
|
+
return decodeURIComponent(text);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Decoders tried against each line; flag if ANY decoded form reveals
|
|
151
|
+
// instruction/exfil keywords. Covers ROT13, reversed text, base64, hex,
|
|
152
|
+
// \u-escape, and %-encoding.
|
|
153
|
+
const DECODERS = [
|
|
154
|
+
rot13,
|
|
155
|
+
(t) => t.split("").reverse().join(""),
|
|
156
|
+
tryBase64,
|
|
157
|
+
tryHex,
|
|
158
|
+
tryUnicodeEsc,
|
|
159
|
+
tryPercent
|
|
160
|
+
];
|
|
161
|
+
function decodesToInstruction(text) {
|
|
162
|
+
return DECODERS.some((decode) => isPrintableInstruction(decode(text)));
|
|
163
|
+
}
|
|
164
|
+
function scanOverrideExfil(file, text, acc) {
|
|
165
|
+
if (OVERRIDE_IGNORE_RE.test(text) ||
|
|
166
|
+
OVERRIDE_DISREGARD_RE.test(text) ||
|
|
167
|
+
OVERRIDE_TAGS_RE.test(text) ||
|
|
168
|
+
OVERRIDE_FORGET_RE.test(text) ||
|
|
169
|
+
OVERRIDE_SAFETY_RE.test(text) ||
|
|
170
|
+
JAILBREAK_RE.test(text) ||
|
|
171
|
+
MULTILANG_OVERRIDE_RE.test(text)) {
|
|
172
|
+
acc.override.push(file);
|
|
173
|
+
}
|
|
174
|
+
if (EXFIL_FETCH_RE.test(text) || EXFIL_SEND_RE.test(text) || MD_BEACON_RE.test(text)) {
|
|
175
|
+
acc.exfil.push(file);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function scanToolPersist(file, text, acc) {
|
|
179
|
+
if (TOOL_DESTRUCTIVE_RE.test(text) ||
|
|
180
|
+
TOOL_SABOTAGE_RE.test(text) ||
|
|
181
|
+
TOOL_IMPERATIVE_DESC_RE.test(text) ||
|
|
182
|
+
TOOL_DISABLE_AUTH_RE.test(text)) {
|
|
183
|
+
acc.toolPoison.push(file);
|
|
184
|
+
}
|
|
185
|
+
if (PERSIST_EVERY_RE.test(text) || PERSIST_START_RE.test(text) || PERSIST_AUTOUPDATE_RE.test(text)) {
|
|
186
|
+
acc.persist.push(file);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function scanAdvanced(file, text, acc) {
|
|
190
|
+
if (REMOTE_INSTR_RE.test(text) || CMD_SUBST_RE.test(text))
|
|
191
|
+
acc.remoteLoad.push(file);
|
|
192
|
+
if (PERM_ESCALATION_RE.test(text))
|
|
193
|
+
acc.permEsc.push(file);
|
|
194
|
+
if (BACKDOOR_RE.test(text))
|
|
195
|
+
acc.backdoor.push(file);
|
|
196
|
+
if (PROMPT_LEAK_RE.test(text))
|
|
197
|
+
acc.promptLeak.push(file);
|
|
198
|
+
}
|
|
199
|
+
function scanHidden(file, text, lines, acc) {
|
|
200
|
+
if (HIDDEN_INVISIBLE_RE.test(text) || HIDDEN_HTML_COMMENT_RE.test(text) || HOMOGLYPH_RE.test(text)) {
|
|
201
|
+
acc.hidden.push(file);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// CSS-hidden text only counts when paired with an imperative on the same line.
|
|
205
|
+
if (lines.some((l) => HIDDEN_CSS_HIDE_RE.test(l) && IMPERATIVE_WORD_RE.test(l))) {
|
|
206
|
+
acc.hidden.push(file);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Encoded blob (base64/hex/\u/%/rot13/reversed) that decodes to instruction keywords.
|
|
210
|
+
if (lines.some((l) => decodesToInstruction(l))) {
|
|
211
|
+
acc.hidden.push(file);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function scanCredMem(file, text, acc) {
|
|
215
|
+
if (CRED_READ_RE.test(text) || CRED_EXFIL_RE.test(text)) {
|
|
216
|
+
acc.cred.push(file);
|
|
217
|
+
}
|
|
218
|
+
if (MEM_FALSEPOS_RE.test(text) || MEM_SUPPRESS_RE.test(text) || MEM_API_RE.test(text)) {
|
|
219
|
+
acc.memory.push(file);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function resolveQuarantineMode() {
|
|
223
|
+
const raw = (process.env["SECURITY_AGENTIC_QUARANTINE"] ?? "").trim().toLowerCase();
|
|
224
|
+
if (!raw || raw === "0" || raw === "false" || raw === "off")
|
|
225
|
+
return "off";
|
|
226
|
+
if (raw === "move" || raw === "quarantine")
|
|
227
|
+
return "move";
|
|
228
|
+
return "strip";
|
|
229
|
+
}
|
|
230
|
+
// Line-level predicate used only when stripping. The multi-line HTML-comment
|
|
231
|
+
// pattern is intentionally excluded — strip operates per line.
|
|
232
|
+
const LINE_MALICIOUS_RES = [
|
|
233
|
+
OVERRIDE_IGNORE_RE, OVERRIDE_DISREGARD_RE, OVERRIDE_TAGS_RE, OVERRIDE_FORGET_RE, OVERRIDE_SAFETY_RE,
|
|
234
|
+
JAILBREAK_RE, MULTILANG_OVERRIDE_RE,
|
|
235
|
+
EXFIL_FETCH_RE, EXFIL_SEND_RE, MD_BEACON_RE,
|
|
236
|
+
TOOL_DESTRUCTIVE_RE, TOOL_SABOTAGE_RE, TOOL_IMPERATIVE_DESC_RE, TOOL_DISABLE_AUTH_RE,
|
|
237
|
+
PERSIST_EVERY_RE, PERSIST_START_RE, PERSIST_AUTOUPDATE_RE,
|
|
238
|
+
HIDDEN_INVISIBLE_RE, HIDDEN_CSS_HIDE_RE, HOMOGLYPH_RE,
|
|
239
|
+
REMOTE_INSTR_RE, CMD_SUBST_RE, PERM_ESCALATION_RE, BACKDOOR_RE, PROMPT_LEAK_RE,
|
|
240
|
+
CRED_READ_RE, CRED_EXFIL_RE,
|
|
241
|
+
MEM_FALSEPOS_RE, MEM_SUPPRESS_RE, MEM_API_RE
|
|
242
|
+
];
|
|
243
|
+
function isMaliciousLine(line) {
|
|
244
|
+
return LINE_MALICIOUS_RES.some((re) => re.test(line)) || decodesToInstruction(line);
|
|
245
|
+
}
|
|
246
|
+
// Resolve a workspace-relative path and reject anything that escapes cwd (CWE-22).
|
|
247
|
+
function safeResolve(relPath) {
|
|
248
|
+
const root = process.cwd();
|
|
249
|
+
const rootPrefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
250
|
+
const p = path.resolve(root, relPath);
|
|
251
|
+
if (p !== root && !p.startsWith(rootPrefix))
|
|
252
|
+
return null;
|
|
253
|
+
return p;
|
|
254
|
+
}
|
|
255
|
+
function stripFile(file) {
|
|
256
|
+
const abs = safeResolve(file);
|
|
257
|
+
if (!abs || !existsSync(abs))
|
|
258
|
+
return `skipped (unresolved path)`;
|
|
259
|
+
let text = "";
|
|
260
|
+
try {
|
|
261
|
+
text = readFileSync(abs, "utf-8");
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return `skipped (unreadable)`;
|
|
265
|
+
}
|
|
266
|
+
const lines = text.split("\n");
|
|
267
|
+
const kept = lines.filter((l) => !isMaliciousLine(l));
|
|
268
|
+
const removed = lines.length - kept.length;
|
|
269
|
+
const outRel = `${file}.sanitized`;
|
|
270
|
+
const outAbs = safeResolve(outRel);
|
|
271
|
+
if (!outAbs)
|
|
272
|
+
return `skipped (unresolved output path)`;
|
|
273
|
+
try {
|
|
274
|
+
writeFileSync(outAbs, kept.join("\n"), "utf-8");
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return `skipped (write failed)`;
|
|
278
|
+
}
|
|
279
|
+
return `stripped ${removed} line(s) → ${outRel} (original left for review)`;
|
|
280
|
+
}
|
|
281
|
+
function moveFile(file) {
|
|
282
|
+
if (file.startsWith(".quarantine/"))
|
|
283
|
+
return `already quarantined`;
|
|
284
|
+
const abs = safeResolve(file);
|
|
285
|
+
if (!abs || !existsSync(abs))
|
|
286
|
+
return `skipped (unresolved path)`;
|
|
287
|
+
const destRel = path.join(".quarantine", file);
|
|
288
|
+
const destAbs = safeResolve(destRel);
|
|
289
|
+
if (!destAbs)
|
|
290
|
+
return `skipped (unresolved destination)`;
|
|
291
|
+
try {
|
|
292
|
+
mkdirSync(path.dirname(destAbs), { recursive: true });
|
|
293
|
+
renameSync(abs, destAbs);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return `skipped (move failed)`;
|
|
297
|
+
}
|
|
298
|
+
return `moved → ${destRel}`;
|
|
299
|
+
}
|
|
300
|
+
/** Apply the configured remediation to every unique flagged file. */
|
|
301
|
+
function applyQuarantine(files, mode) {
|
|
302
|
+
const out = new Map();
|
|
303
|
+
if (mode === "off")
|
|
304
|
+
return out;
|
|
305
|
+
for (const file of files) {
|
|
306
|
+
out.set(file, mode === "move" ? moveFile(file) : stripFile(file));
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
function uniqueFlaggedFiles(acc) {
|
|
311
|
+
const all = [
|
|
312
|
+
...acc.override, ...acc.exfil, ...acc.toolPoison, ...acc.persist,
|
|
313
|
+
...acc.hidden, ...acc.cred, ...acc.memory,
|
|
314
|
+
...acc.remoteLoad, ...acc.permEsc, ...acc.backdoor, ...acc.promptLeak
|
|
315
|
+
];
|
|
316
|
+
return Array.from(new Set(all));
|
|
317
|
+
}
|
|
318
|
+
function remediationNote(files, remediation) {
|
|
319
|
+
const outcomes = files
|
|
320
|
+
.filter((f) => remediation.has(f))
|
|
321
|
+
.map((f) => `${f}: ${remediation.get(f)}`);
|
|
322
|
+
if (outcomes.length === 0)
|
|
323
|
+
return null;
|
|
324
|
+
return `AUTO-REMEDIATION applied (SECURITY_AGENTIC_QUARANTINE) — ${outcomes.join("; ")}. Verify before trusting the result.`;
|
|
325
|
+
}
|
|
326
|
+
function buildFindings(acc, remediation) {
|
|
327
|
+
const findings = [];
|
|
328
|
+
const withNote = (files, actions) => {
|
|
329
|
+
const note = remediationNote(files, remediation);
|
|
330
|
+
return note ? [...actions, note] : actions;
|
|
331
|
+
};
|
|
332
|
+
if (acc.override.length > 0) {
|
|
333
|
+
findings.push({
|
|
334
|
+
id: "AGENT_INSTRUCTION_OVERRIDE",
|
|
335
|
+
title: "Agentic instruction file contains prompt-override / instruction-hijack directives",
|
|
336
|
+
severity: "CRITICAL",
|
|
337
|
+
files: acc.override,
|
|
338
|
+
evidence: acc.override,
|
|
339
|
+
requiredActions: withNote(acc.override, [
|
|
340
|
+
"Treat this instruction file as hostile: an AI agent reading it can be hijacked via embedded 'ignore previous instructions', <system> tags, or 'you are now' directives (OWASP LLM01, MITRE ATLAS AML.T0051, CWE-77).",
|
|
341
|
+
"Quarantine the file and trace its origin (commit author, PR, supply-chain source) before any agent ingests the repository.",
|
|
342
|
+
"Enforce instruction-hierarchy isolation in agent runtimes: render repo-sourced instruction files as untrusted DATA inside delimited boundaries, never as system authority."
|
|
343
|
+
])
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (acc.exfil.length > 0) {
|
|
347
|
+
findings.push({
|
|
348
|
+
id: "AGENT_INSTRUCTION_EXFIL",
|
|
349
|
+
title: "Agentic instruction file directs the agent to exfiltrate data to an external host",
|
|
350
|
+
severity: "CRITICAL",
|
|
351
|
+
files: acc.exfil,
|
|
352
|
+
evidence: acc.exfil,
|
|
353
|
+
requiredActions: withNote(acc.exfil, [
|
|
354
|
+
"Remove directives that instruct the agent to fetch/curl/POST to non-allowlisted hosts or to send env/secrets/tokens off-box (MITRE ATLAS AML.T0024, CWE-200).",
|
|
355
|
+
"Apply an egress allowlist to the agent's tool runtime so instruction-driven exfiltration calls are blocked at execution time.",
|
|
356
|
+
"Rotate any credentials reachable by the agent if there is evidence the instruction file was active during a run."
|
|
357
|
+
])
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (acc.toolPoison.length > 0) {
|
|
361
|
+
findings.push({
|
|
362
|
+
id: "AGENT_TOOL_POISONING",
|
|
363
|
+
title: "Agentic instruction / tool definition encodes destructive or unscoped tool behavior",
|
|
364
|
+
severity: "HIGH",
|
|
365
|
+
files: acc.toolPoison,
|
|
366
|
+
evidence: acc.toolPoison,
|
|
367
|
+
requiredActions: withNote(acc.toolPoison, [
|
|
368
|
+
"Inspect tool/MCP 'description' fields and instruction bodies for destructive commands (rm -rf, eval, shell exec) or hidden imperatives ('always run', 'do not tell the user') — these poison the model's tool-use plane (MITRE ATLAS AML.T0054, CWE-94).",
|
|
369
|
+
"Define MCP tool descriptions as static, code-reviewed constants; reject any tool whose description carries instructions to the model rather than a neutral capability summary.",
|
|
370
|
+
"Run agent tools under least privilege with an explicit allowlist; deny directives that disable auth, validation, or the sandbox."
|
|
371
|
+
])
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (acc.persist.length > 0) {
|
|
375
|
+
findings.push({
|
|
376
|
+
id: "AGENT_PERSISTENCE_DIRECTIVE",
|
|
377
|
+
title: "Agentic instruction file contains self-persistence / auto-reinstall directives",
|
|
378
|
+
severity: "HIGH",
|
|
379
|
+
files: acc.persist,
|
|
380
|
+
evidence: acc.persist,
|
|
381
|
+
requiredActions: withNote(acc.persist, [
|
|
382
|
+
"Strip 'on every invocation', 'at the start of every run', and auto-update/ensure_skill directives — they let a malicious instruction set survive removal (persistence; MITRE ATLAS AML.T0051).",
|
|
383
|
+
"Pin and integrity-check (SHA-256) any skill/agent definition the repo loads; forbid runtime self-modification or self-reinstallation.",
|
|
384
|
+
"Audit version history of the file for a benign-then-weaponized edit pattern."
|
|
385
|
+
])
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (acc.hidden.length > 0) {
|
|
389
|
+
findings.push({
|
|
390
|
+
id: "AGENT_HIDDEN_INSTRUCTION",
|
|
391
|
+
title: "Agentic instruction file hides instructions via invisible characters, HTML comments, or encoded payloads",
|
|
392
|
+
severity: "CRITICAL",
|
|
393
|
+
files: acc.hidden,
|
|
394
|
+
evidence: acc.hidden,
|
|
395
|
+
requiredActions: withNote(acc.hidden, [
|
|
396
|
+
"Inspect the file for zero-width/bidi Unicode (U+200B–U+200F, U+202A–U+202E, U+2060–U+2069, U+FEFF), HTML comments, CSS-hidden text, and base64 blobs that decode to instructions — all smuggle directives past human review (CWE-116, MITRE ATLAS AML.T0051).",
|
|
397
|
+
"Normalize instruction files to NFC and strip non-printable characters before any agent ingests them; add a pre-commit hook that rejects invisible characters.",
|
|
398
|
+
"Decode and review every embedded base64/hex blob; treat any that decodes to imperatives or URLs as a live injection payload."
|
|
399
|
+
])
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (acc.cred.length > 0) {
|
|
403
|
+
findings.push({
|
|
404
|
+
id: "AGENT_CREDENTIAL_HARVEST",
|
|
405
|
+
title: "Agentic instruction file directs the agent to read or reveal credentials",
|
|
406
|
+
severity: "CRITICAL",
|
|
407
|
+
files: acc.cred,
|
|
408
|
+
evidence: acc.cred,
|
|
409
|
+
requiredActions: withNote(acc.cred, [
|
|
410
|
+
"Remove directives instructing the agent to read .env, ~/.aws/credentials, ~/.ssh, keychains, or to dump process.env / print secrets (credential access; MITRE ATLAS AML.T0024, CWE-522).",
|
|
411
|
+
"Run agents with secrets injected out-of-band and scoped to least privilege so instruction-driven harvesting yields nothing useful.",
|
|
412
|
+
"Rotate any credentials the agent could reach and review run logs for prior harvesting attempts."
|
|
413
|
+
])
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (acc.memory.length > 0) {
|
|
417
|
+
findings.push({
|
|
418
|
+
id: "AGENT_MEMORY_POISONING",
|
|
419
|
+
title: "Agentic instruction file directs the agent to poison memory or suppress findings",
|
|
420
|
+
severity: "HIGH",
|
|
421
|
+
files: acc.memory,
|
|
422
|
+
evidence: acc.memory,
|
|
423
|
+
requiredActions: withNote(acc.memory, [
|
|
424
|
+
"Remove directives that tell the agent to write false-positive entries, whitelist findings, or mark vulnerabilities as safe/resolved — these blind future scans (data poisoning; MITRE ATLAS AML.T0051).",
|
|
425
|
+
"Make agent memory/finding-suppression writes require validated, authenticated provenance; never accept suppression instructions sourced from a scanned repository.",
|
|
426
|
+
"Audit existing agent memory for entries that may have been planted by this directive."
|
|
427
|
+
])
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (acc.remoteLoad.length > 0) {
|
|
431
|
+
findings.push({
|
|
432
|
+
id: "AGENT_REMOTE_INSTRUCTION_LOAD",
|
|
433
|
+
title: "Agentic instruction file pulls instructions or commands from an external location",
|
|
434
|
+
severity: "CRITICAL",
|
|
435
|
+
files: acc.remoteLoad,
|
|
436
|
+
evidence: acc.remoteLoad,
|
|
437
|
+
requiredActions: withNote(acc.remoteLoad, [
|
|
438
|
+
"Remove directives that load/fetch/follow instructions from a URL or run command-substitution ($(curl …), `wget …`) — the visible file looks clean while the real payload arrives at runtime (indirect injection; OWASP LLM01, MITRE ATLAS AML.T0051).",
|
|
439
|
+
"Forbid agents from following instructions sourced from any network location; all agent authority must come from reviewed, pinned local files.",
|
|
440
|
+
"Apply an egress allowlist so runtime instruction-fetching is blocked even if the directive survives review."
|
|
441
|
+
])
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (acc.permEsc.length > 0) {
|
|
445
|
+
findings.push({
|
|
446
|
+
id: "AGENT_PERMISSION_ESCALATION",
|
|
447
|
+
title: "Agentic instruction file requests elevated permissions or tool access",
|
|
448
|
+
severity: "HIGH",
|
|
449
|
+
files: acc.permEsc,
|
|
450
|
+
evidence: acc.permEsc,
|
|
451
|
+
requiredActions: withNote(acc.permEsc, [
|
|
452
|
+
"Remove requests to skip permissions (--dangerously-skip-permissions, bypassPermissions, auto-approve), broaden allowed-tools to wildcards (Bash(*)), or run without confirmation — repo-sourced files must never widen the agent's own privileges (excessive agency; OWASP LLM08, CWE-269).",
|
|
453
|
+
"Pin the agent's permission mode and tool allowlist in trusted operator config, never in repo-readable instruction files.",
|
|
454
|
+
"Require human approval for any change to allowed-tools or permission scope."
|
|
455
|
+
])
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (acc.backdoor.length > 0) {
|
|
459
|
+
findings.push({
|
|
460
|
+
id: "AGENT_BACKDOOR_INSERT",
|
|
461
|
+
title: "Agentic instruction file directs the agent to insert a backdoor or persistent access",
|
|
462
|
+
severity: "CRITICAL",
|
|
463
|
+
files: acc.backdoor,
|
|
464
|
+
evidence: acc.backdoor,
|
|
465
|
+
requiredActions: withNote(acc.backdoor, [
|
|
466
|
+
"Remove directives to add SSH keys / authorized_keys, create admin accounts, plant reverse/bind shells, add webhooks, hardcode credentials, or disable MFA/signature checks (persistence + privilege escalation; MITRE ATT&CK T1098, CWE-912).",
|
|
467
|
+
"Treat the repository as potentially compromised: diff for any backdoor the agent may already have written, and review authorized_keys / IAM / webhook config.",
|
|
468
|
+
"Block agent write-access to auth-sensitive paths (authorized_keys, IAM policies, CI secrets) entirely."
|
|
469
|
+
])
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (acc.promptLeak.length > 0) {
|
|
473
|
+
findings.push({
|
|
474
|
+
id: "AGENT_PROMPT_LEAK",
|
|
475
|
+
title: "Agentic instruction file attempts to extract the agent's system prompt or instructions",
|
|
476
|
+
severity: "MEDIUM",
|
|
477
|
+
files: acc.promptLeak,
|
|
478
|
+
evidence: acc.promptLeak,
|
|
479
|
+
requiredActions: withNote(acc.promptLeak, [
|
|
480
|
+
"Remove directives asking the agent to repeat/print/reveal its system prompt, rules, or configuration — prompt-leak is reconnaissance that enables a tailored jailbreak (MITRE ATLAS AML.T0056).",
|
|
481
|
+
"Configure the agent runtime to refuse system-prompt disclosure and to treat such requests as adversarial probes.",
|
|
482
|
+
"Log and alert on prompt-extraction attempts as a precursor to a targeted attack."
|
|
483
|
+
])
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return findings;
|
|
487
|
+
}
|
|
488
|
+
export async function checkAgenticInstructions(_) {
|
|
489
|
+
const files = await fg(AGENTIC_GLOBS, {
|
|
490
|
+
dot: true,
|
|
491
|
+
onlyFiles: true,
|
|
492
|
+
followSymbolicLinks: false,
|
|
493
|
+
ignore: AGENTIC_IGNORE
|
|
494
|
+
});
|
|
495
|
+
const acc = makeAcc();
|
|
496
|
+
for (const file of files) {
|
|
497
|
+
let text = "";
|
|
498
|
+
try {
|
|
499
|
+
text = await readFileSafe(file);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const lines = text.split("\n");
|
|
505
|
+
scanOverrideExfil(file, text, acc);
|
|
506
|
+
scanToolPersist(file, text, acc);
|
|
507
|
+
scanAdvanced(file, text, acc);
|
|
508
|
+
scanHidden(file, text, lines, acc);
|
|
509
|
+
scanCredMem(file, text, acc);
|
|
510
|
+
}
|
|
511
|
+
// Opt-in remediation: only runs when SECURITY_AGENTIC_QUARANTINE is set.
|
|
512
|
+
// Detection-only by default — never mutates the repo unless explicitly enabled.
|
|
513
|
+
const remediation = applyQuarantine(uniqueFlaggedFiles(acc), resolveQuarantineMode());
|
|
514
|
+
return buildFindings(acc, remediation);
|
|
515
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
3
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// AI governance / shadow-AI gap closers.
|
|
5
|
+
//
|
|
6
|
+
// Missing-control detection for AI threats that pure code-pattern matching can
|
|
7
|
+
// only partially see:
|
|
8
|
+
// • AI_BIAS_TESTING_ABSENT — ML decision systems without fairness tests
|
|
9
|
+
// • AI_SHADOW_EXFIL_SECRET_TO_LLM — secrets/PII flowing into an LLM payload
|
|
10
|
+
// • AI_DEEPFAKE_VERIFICATION_ABSENT — high-value flows without out-of-band verify
|
|
11
|
+
//
|
|
12
|
+
// Maps to EU AI Act, NIST AI RMF, ISO 42001, OWASP LLM06 (Sensitive Info
|
|
13
|
+
// Disclosure), CWE-200 / CWE-1395.
|
|
14
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
const SOURCE_FILE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|json)$/i;
|
|
16
|
+
const GLOB_IGNORE = [
|
|
17
|
+
"**/node_modules/**",
|
|
18
|
+
"**/.git/**",
|
|
19
|
+
"**/dist/**",
|
|
20
|
+
"**/.mcp/**"
|
|
21
|
+
];
|
|
22
|
+
// ─── AI_BIAS_TESTING_ABSENT ──────────────────────────────────────────────────
|
|
23
|
+
// ML inference/decision code paired with a consequential domain, lacking any
|
|
24
|
+
// fairness-testing artifact anywhere in the repo.
|
|
25
|
+
const ML_PREDICT_RE = /\.(?:predict|predict_proba|classify|decision_function|score)\s*\(|model\.(?:predict|forward|infer)|\b(?:RandomForest|XGB|LogisticRegression|GradientBoosting|Sequential|sklearn|tensorflow|torch)\b/i;
|
|
26
|
+
const DECISION_DOMAIN_RE = /\b(?:hir(?:e|ing)|applicant|candidate|resume|loan|credit(?:[_-]?score|worthy)?|lending|underwrit\w+|insurance|eligib\w+|recidiv\w+|parole|admission|tenant\s+screen|risk[_-]?score)\b/i;
|
|
27
|
+
const FAIRNESS_ARTIFACT_RE = /\bfairlearn\b|\baif360\b|\baequitas\b|disparate[_-]?impact|equal(?:ized)?[_-]?odds|demographic[_-]?parity|\bfairness\b|bias[_-]?(?:test|audit|metric|check)|protected[_-]?attribute/i;
|
|
28
|
+
// ─── AI_SHADOW_EXFIL_SECRET_TO_LLM ───────────────────────────────────────────
|
|
29
|
+
const SECRET_ID_RE = /process\.env\.[A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|PRIVATE)|\b(?:apiKey|secretKey|accessToken|privateKey|clientSecret)\b|\bssn\b|cardNumber|\.env\b/i;
|
|
30
|
+
const LLM_PAYLOAD_RE = /(?:messages|prompt|systemPrompt|userMessage|content|input)\s*[:=]|\.(?:chat\.completions\.create|messages\.create|completions\.create|generateContent|invoke)\s*\(|\bopenai\.|\banthropic\.|\bllm\./i;
|
|
31
|
+
// ─── AI_DEEPFAKE_VERIFICATION_ABSENT ─────────────────────────────────────────
|
|
32
|
+
const HIGH_VALUE_FLOW_RE = /\b(?:wireTransfer|approveTransfer|sendMoney|transferFunds|resetPassword|changePassword|grantAccess|approvePayment|payout|disburse|releaseFunds|changeBankAccount)\b/i;
|
|
33
|
+
const OOB_VERIFY_RE = /out[_-]?of[_-]?band|callback\s+verif\w+|step[_-]?up\s+auth|\bMFA\b|\bOTP\b|verify\s+identity|liveness|deepfake|second\s+factor|known[_-]?good\s+number/i;
|
|
34
|
+
// Returns true if `targetRe` matches within `window` lines of any `anchorRe` line.
|
|
35
|
+
function windowMatch(lines, anchorRe, targetRe, window) {
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
if (!anchorRe.test(lines[i]))
|
|
38
|
+
continue;
|
|
39
|
+
const start = Math.max(0, i - window);
|
|
40
|
+
const end = Math.min(lines.length - 1, i + window);
|
|
41
|
+
for (let j = start; j <= end; j++) {
|
|
42
|
+
if (targetRe.test(lines[j]))
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function scanFile(file, text, sig) {
|
|
49
|
+
if (FAIRNESS_ARTIFACT_RE.test(text))
|
|
50
|
+
sig.fairnessArtifact = true;
|
|
51
|
+
if (ML_PREDICT_RE.test(text) && DECISION_DOMAIN_RE.test(text))
|
|
52
|
+
sig.mlDecision = true;
|
|
53
|
+
if (HIGH_VALUE_FLOW_RE.test(text))
|
|
54
|
+
sig.highValueFlow = true;
|
|
55
|
+
if (OOB_VERIFY_RE.test(text))
|
|
56
|
+
sig.oobVerify = true;
|
|
57
|
+
if (SECRET_ID_RE.test(text) && LLM_PAYLOAD_RE.test(text)) {
|
|
58
|
+
const lines = text.split("\n");
|
|
59
|
+
if (windowMatch(lines, SECRET_ID_RE, LLM_PAYLOAD_RE, 15)) {
|
|
60
|
+
sig.shadowExfilFiles.push(file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildFindings(sig) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
if (sig.mlDecision && !sig.fairnessArtifact) {
|
|
67
|
+
findings.push({
|
|
68
|
+
id: "AI_BIAS_TESTING_ABSENT",
|
|
69
|
+
title: "ML decision system detected with no fairness / bias-testing artifact",
|
|
70
|
+
severity: "MEDIUM",
|
|
71
|
+
requiredActions: [
|
|
72
|
+
"Add fairness evaluation (e.g. Fairlearn, AIF360, Aequitas) measuring disparate impact, equalized odds, and demographic parity across protected attributes for any model that affects people (EU AI Act high-risk obligations; NIST AI RMF MEASURE 2.11 / MANAGE).",
|
|
73
|
+
"Document the training-data representativeness assessment and bias-mitigation steps as model-card evidence (ISO 42001).",
|
|
74
|
+
"Gate model promotion on fairness thresholds in CI and re-evaluate on every retrain to catch drift-induced bias."
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (sig.shadowExfilFiles.length > 0) {
|
|
79
|
+
findings.push({
|
|
80
|
+
id: "AI_SHADOW_EXFIL_SECRET_TO_LLM",
|
|
81
|
+
title: "Secrets or PII interpolated into an LLM request payload — shadow-AI data leakage",
|
|
82
|
+
severity: "HIGH",
|
|
83
|
+
files: sig.shadowExfilFiles,
|
|
84
|
+
evidence: sig.shadowExfilFiles,
|
|
85
|
+
requiredActions: [
|
|
86
|
+
"Never place secrets, API keys, or raw PII into prompt/messages content — they leave your trust boundary and may be retained or logged by the model provider (OWASP LLM06, CWE-200).",
|
|
87
|
+
"Insert a redaction/tokenization step (e.g. Microsoft Presidio) at the prompt-construction boundary and pass only opaque references to the LLM.",
|
|
88
|
+
"Add a DLP guard and CI check that fails when process.env secrets or PII identifiers reach an LLM SDK call site."
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (sig.highValueFlow && !sig.oobVerify) {
|
|
93
|
+
findings.push({
|
|
94
|
+
id: "AI_DEEPFAKE_VERIFICATION_ABSENT",
|
|
95
|
+
title: "High-value action flow without out-of-band identity verification — AI deepfake / vishing exposure",
|
|
96
|
+
severity: "MEDIUM",
|
|
97
|
+
requiredActions: [
|
|
98
|
+
"Require out-of-band verification (callback to a known-good number, step-up MFA/OTP, or liveness check) before executing high-value actions — AI voice/video clones now routinely defeat single-channel approval (MITRE ATLAS AML.T0052 social engineering).",
|
|
99
|
+
"Never treat a phone call, voicemail, or video request as sufficient authorization for fund transfers, account changes, or access grants.",
|
|
100
|
+
"Add transaction anomaly checks and a mandatory second approver for irreversible high-value operations."
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
export async function checkAiGovernance(_) {
|
|
107
|
+
const files = await fg(["**/*.*"], {
|
|
108
|
+
dot: true,
|
|
109
|
+
onlyFiles: true,
|
|
110
|
+
ignore: GLOB_IGNORE
|
|
111
|
+
});
|
|
112
|
+
const sig = {
|
|
113
|
+
mlDecision: false,
|
|
114
|
+
fairnessArtifact: false,
|
|
115
|
+
shadowExfilFiles: [],
|
|
116
|
+
highValueFlow: false,
|
|
117
|
+
oobVerify: false
|
|
118
|
+
};
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
if (!SOURCE_FILE_RE.test(file))
|
|
121
|
+
continue;
|
|
122
|
+
let text = "";
|
|
123
|
+
try {
|
|
124
|
+
text = await readFileSafe(file);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
scanFile(file, text, sig);
|
|
130
|
+
}
|
|
131
|
+
return buildFindings(sig);
|
|
132
|
+
}
|
package/dist/gate/checks/ai.js
CHANGED
|
@@ -79,7 +79,7 @@ const MEMORY_WRITE_RE = /(?:memory\.add|memory\.save|memory\.set|memoryStore\.wr
|
|
|
79
79
|
// ─── AI_RAG_CORPUS_POISONING ──────────────────────────────────────────────────
|
|
80
80
|
const VECTOR_UPSERT_RE = /(?:upsert|addDocuments|add_documents|indexDocuments|ingestDocument|vectorStore\.add|\.from_documents)\s*\([^)]*(?:userInput|req\.body|req\.file|formData|upload)/i;
|
|
81
81
|
// ─── AI_TOKEN_SMUGGLING ───────────────────────────────────────────────────────
|
|
82
|
-
const ZERO_WIDTH_RE =
|
|
82
|
+
const ZERO_WIDTH_RE = /\u200b|\u200c|\u200d|\u200e|\u200f|\u2060|\ufeff|\u202e|\u202f|\u2028|\u2029|\u00ad/;
|
|
83
83
|
// ─── AI_AGENTIC_PRIVILEGE_ESCALATION ─────────────────────────────────────────
|
|
84
84
|
const TOOL_REGISTER_RE = /(?:tools\.push|tools\.add|registerTool|addTool|extend_tools|capabilities\.push)\s*\([^)]*(?:response|output|completion|llm|agent)/i;
|
|
85
85
|
// ─── AI_LLM_JUDGE_MANIPULATION ───────────────────────────────────────────────
|