security-mcp 1.1.4 → 1.3.3
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 +341 -1018
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- 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/defaults/security-policy.json +2 -2
- 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/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +582 -15
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +955 -2
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +256 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- 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/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +202 -9
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +316 -130
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +131 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +161 -24
- package/dist/mcp/orchestration.js +377 -89
- package/dist/mcp/server.js +460 -69
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +37 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +56 -3
- package/dist/tests/run.js +124 -1
- package/package.json +9 -9
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +118 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +377 -0
- package/skills/ai-llm-redteam/SKILL.md +113 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
- package/skills/android-penetration-tester/SKILL.md +464 -46
- package/skills/anti-replay-tester/SKILL.md +115 -0
- package/skills/appsec-code-auditor/SKILL.md +94 -0
- package/skills/artifact-integrity-analyst/SKILL.md +450 -0
- package/skills/attack-navigator/SKILL.md +476 -8
- package/skills/auth-session-hacker/SKILL.md +111 -0
- package/skills/aws-penetration-tester/SKILL.md +510 -0
- package/skills/azure-penetration-tester/SKILL.md +542 -3
- package/skills/binary-auth-validator/SKILL.md +120 -0
- package/skills/bot-detection-specialist/SKILL.md +118 -0
- package/skills/business-logic-attacker/SKILL.md +240 -0
- package/skills/capec-code-mapper/SKILL.md +93 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
- package/skills/ciso-orchestrator/SKILL.md +465 -43
- package/skills/cloud-infra-specialist/SKILL.md +127 -0
- package/skills/compliance-gap-analyst/SKILL.md +431 -0
- package/skills/compliance-grc/SKILL.md +94 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +111 -0
- package/skills/crypto-pki-specialist/SKILL.md +96 -0
- package/skills/csa-ccm-mapper/SKILL.md +93 -0
- package/skills/csf2-governance-mapper/SKILL.md +93 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +118 -0
- package/skills/dependency-confusion-attacker/SKILL.md +424 -0
- package/skills/device-integrity-aggregator/SKILL.md +117 -0
- package/skills/dos-resilience-tester/SKILL.md +106 -0
- package/skills/dread-scorer/SKILL.md +93 -0
- package/skills/egress-policy-enforcer/SKILL.md +108 -0
- package/skills/evidence-collector/SKILL.md +107 -0
- package/skills/file-upload-attacker/SKILL.md +118 -0
- package/skills/gcp-penetration-tester/SKILL.md +510 -2
- package/skills/git-history-secret-scanner/SKILL.md +115 -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 +161 -0
- package/skills/incident-responder/SKILL.md +120 -0
- package/skills/injection-specialist/SKILL.md +111 -0
- package/skills/ios-security-auditor/SKILL.md +291 -0
- package/skills/json-ambiguity-tester/SKILL.md +145 -0
- package/skills/k8s-container-escaper/SKILL.md +406 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
- package/skills/kill-switch-engineer/SKILL.md +111 -0
- package/skills/linddun-privacy-analyst/SKILL.md +111 -0
- package/skills/logic-race-fuzzer/SKILL.md +452 -0
- package/skills/mobile-api-network-attacker/SKILL.md +430 -0
- package/skills/mobile-binary-hardener/SKILL.md +111 -0
- package/skills/mobile-security-specialist/SKILL.md +94 -0
- package/skills/mobile-webview-auditor/SKILL.md +105 -0
- package/skills/model-extraction-attacker/SKILL.md +228 -0
- package/skills/multipart-abuse-tester/SKILL.md +93 -0
- package/skills/oauth-pkce-specialist/SKILL.md +113 -0
- package/skills/parser-exhaustion-tester/SKILL.md +151 -0
- package/skills/pentest-infra/SKILL.md +107 -0
- package/skills/pentest-social/SKILL.md +210 -0
- package/skills/pentest-team/SKILL.md +96 -0
- package/skills/pentest-web-api/SKILL.md +107 -0
- package/skills/privacy-flow-analyst/SKILL.md +243 -0
- package/skills/prompt-injection-specialist/SKILL.md +403 -0
- package/skills/quantum-migration-planner/SKILL.md +105 -0
- package/skills/rag-poisoning-specialist/SKILL.md +367 -0
- package/skills/registry-mirror-enforcer/SKILL.md +93 -0
- package/skills/rotation-validation-agent/SKILL.md +121 -0
- package/skills/samm-assessor/SKILL.md +94 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
- package/skills/senior-security-engineer/SKILL.md +178 -0
- package/skills/serialization-memory-attacker/SKILL.md +341 -0
- package/skills/session-timeout-tester/SKILL.md +170 -0
- package/skills/slsa-level3-enforcer/SKILL.md +121 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
- package/skills/ssrf-detection-validator/SKILL.md +117 -0
- package/skills/step-up-auth-enforcer/SKILL.md +93 -0
- package/skills/stride-pasta-analyst/SKILL.md +429 -0
- package/skills/supply-chain-devsecops/SKILL.md +107 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
- package/skills/threat-modeler/SKILL.md +94 -0
- package/skills/tls-certificate-auditor/SKILL.md +582 -18
- package/skills/token-reuse-detector/SKILL.md +104 -0
- package/skills/trike-risk-modeler/SKILL.md +93 -0
- package/skills/unicode-homograph-tester/SKILL.md +93 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
- package/skills/webhook-security-tester/SKILL.md +111 -0
- package/skills/zero-trust-architect/SKILL.md +118 -0
package/dist/gate/checks/ai.js
CHANGED
|
@@ -1,70 +1,788 @@
|
|
|
1
1
|
import fg from "fast-glob";
|
|
2
2
|
import { readFileSafe } from "../../repo/fs.js";
|
|
3
|
+
import { searchRepo } from "../../repo/search.js";
|
|
3
4
|
const SOURCE_FILE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|json)$/i;
|
|
5
|
+
// ─── Existing check regexes ────────────────────────────────────────────────
|
|
4
6
|
const SCHEMA_RE = /zod\.object\(|outputSchema|json_schema|JSON schema/i;
|
|
5
7
|
const TOOL_RE = /\bfunction_call\b|\btools?\b\s*[:=]/i;
|
|
6
8
|
const INJECTION_RE = /system prompt|developer message|ignore previous|prompt injection/i;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
9
|
+
// ─── AI_PROMPT_CONCAT ────────────────────────────────────────────────────────
|
|
10
|
+
// System prompt built by direct concatenation with user input.
|
|
11
|
+
// Split into two patterns to keep each under the complexity threshold.
|
|
12
|
+
const PROMPT_CONCAT_A_RE = /systemPrompt\s*\+\s*\w+Input|`\$\{systemPrompt\}\$\{user/i;
|
|
13
|
+
const PROMPT_CONCAT_B_RE = /`\$\{system[^}]*\}\$\{[^}]*message|\[\s*\.{3}systemParts/i;
|
|
14
|
+
// ─── AI_OUTPUT_TO_EVAL ───────────────────────────────────────────────────────
|
|
15
|
+
// LLM response fed into eval / exec / spawn.
|
|
16
|
+
// Two regexes: one for eval/exec, one for spawn, to stay under complexity limit.
|
|
17
|
+
const OUTPUT_TO_EVAL_A_RE = /eval\s*\([^)]*(?:response|completion|output|result)/i;
|
|
18
|
+
const OUTPUT_TO_EVAL_B_RE = /exec\s*\([^)]*(?:response|completion|output)|spawn\s*\([^)]*(?:response|output)/i;
|
|
19
|
+
// ─── AI_PII_IN_PROMPT ────────────────────────────────────────────────────────
|
|
20
|
+
// PII field names embedded in prompt template literals.
|
|
21
|
+
// Split by direction (PII-then-prompt vs prompt-then-PII) to reduce per-regex complexity.
|
|
22
|
+
const PII_FIELDS_FRAG = "ssn|socialSecurity|cardNumber|cvv|password|secret";
|
|
23
|
+
const PROMPT_KEYS_FRAG = "messages|prompt|system";
|
|
24
|
+
const PII_IN_PROMPT_A_RE = new RegExp("`[^`]*(?:" + PII_FIELDS_FRAG + ")[^`]*`[^`]*(?:" + PROMPT_KEYS_FRAG + ")", "i");
|
|
25
|
+
const PII_IN_PROMPT_B_RE = new RegExp("(?:" + PROMPT_KEYS_FRAG + ")[^`]*`[^`]*(?:" + PII_FIELDS_FRAG + ")[^`]*`", "i");
|
|
26
|
+
// ─── AI_RATE_LIMIT_MISSING ───────────────────────────────────────────────────
|
|
27
|
+
const AI_ROUTE_HANDLER_RE = /(?:router|app)\s*\.\s*(?:get|post|put|patch|delete)\s*\(/i;
|
|
28
|
+
const RATE_LIMIT_PRESENT_RE = /rateLimit|rate_limit|rateLimiter|ThrottlerGuard|upstash\/ratelimit/i;
|
|
29
|
+
const AI_SDK_CALL_RE = /openai\.|anthropic\.|\.chat\.completions\.create|\.messages\.create/i;
|
|
30
|
+
// ─── AI_RAG_AUTHZ_MISSING ────────────────────────────────────────────────────
|
|
31
|
+
const VECTOR_SEARCH_RE = /similarity_search|vectorSearch|\.retrieve\s*\(|\.search\s*\([^)]*embed/i;
|
|
32
|
+
const AUTHZ_NEARBY_RE = /userId|tenantId|authorize|hasPermission|checkAccess/i;
|
|
33
|
+
// ─── AI_TOOL_ARGS_UNVALIDATED ────────────────────────────────────────────────
|
|
34
|
+
const TOOL_ARGS_HANDLER_RE = /(?:function_call|tool_call|toolCall)[^}]*(?:arguments|args)\s*[:=]/i;
|
|
35
|
+
const TOOL_ARGS_VALIDATED_RE = /\.parse\s*\(|z\.object|\.safeParse|ajv\.validate|joi\.validate/i;
|
|
36
|
+
// ─── AI_STREAMING_NO_TIMEOUT ─────────────────────────────────────────────────
|
|
37
|
+
const STREAMING_CALL_RE = /stream\s*:\s*true|\.stream\s*\(/i;
|
|
38
|
+
const STREAM_SAFEGUARD_RE = /AbortController|AbortSignal|setTimeout|signal\s*:|timeout\s*:/i;
|
|
39
|
+
// ─── AI_MULTI_TENANT_CONTEXT_LEAK ────────────────────────────────────────────
|
|
40
|
+
const CONTEXT_BUILD_RE = /(?:history|context|messages)\s*\.push\s*\(|\.concat\s*\([^)]*message/i;
|
|
41
|
+
const TENANT_FILTER_RE = /userId|tenantId|orgId|accountId/i;
|
|
42
|
+
// ─── AI_AGENT_UNBOUNDED_LOOP ──────────────────────────────────────────────────
|
|
43
|
+
const AGENT_LOOP_RE = /while\s*\([^)]*tool_calls|for\s*\([^)]*agent.*step|while\s*\([^)]*has_more/i;
|
|
44
|
+
const ITERATION_CAP_RE = /maxIterations|max_iterations|MAX_STEPS|iteration\s*[<>]=?\s*\d/i;
|
|
45
|
+
// ─── AI_SYSTEM_PROMPT_HARDCODED ───────────────────────────────────────────────
|
|
46
|
+
// Secrets embedded inside template-literal system prompts.
|
|
47
|
+
// Split across two regexes to stay under complexity threshold.
|
|
48
|
+
const HARDCODED_SECRET_A_RE = /`[^`]*(?:OPENAI_API_KEY|sk-[A-Z0-9]{20,})[^`]*`/i;
|
|
49
|
+
const HARDCODED_SECRET_B_RE = /`[^`]*(?:password|secret)\s*=\s*['"][^'"]+['"][^`]*`/i;
|
|
50
|
+
// ─── AI_MODEL_VERSION_UNPINNED ────────────────────────────────────────────────
|
|
51
|
+
const MODEL_UNPINNED_RE = /['"](?:gpt-4|gpt-3\.5-turbo|claude-3-(?:opus|sonnet|haiku)|claude-2)['"]/i;
|
|
52
|
+
const MODEL_PINNED_RE = /['"](?:gpt-4-\d{4}|gpt-3\.5-turbo-\d{4}|claude-(?:opus|sonnet|haiku)-\d|claude-sonnet-\d-\d)['"]/i;
|
|
53
|
+
// ─── AI_LANGCHAIN_DANGEROUS_TOOLS ─────────────────────────────────────────────
|
|
54
|
+
const LANGCHAIN_DANGEROUS_RE = /PythonREPLTool|BashTool|ShellTool|SystemCommandTool/i;
|
|
55
|
+
// ─── AI_HUGGINGFACE_UNPINNED ──────────────────────────────────────────────────
|
|
56
|
+
const HF_PRETRAINED_RE = /from_pretrained\s*\(/i;
|
|
57
|
+
const HF_REVISION_RE = /revision\s*=/i;
|
|
58
|
+
// ─── AI_EMBEDDING_UNAUTH ──────────────────────────────────────────────────────
|
|
59
|
+
const VECTOR_CLIENT_RE = /new\s+(?:PineconeClient|Pinecone|Chroma|QdrantClient|WeaviateClient|MilvusClient)\s*\(/i;
|
|
60
|
+
const VECTOR_AUTH_RE = /api_key|apiKey|auth_token|authToken|environment\s*=|username\s*=/i;
|
|
61
|
+
// ─── AI_FINE_TUNE_DATA_PII ────────────────────────────────────────────────────
|
|
62
|
+
const FINE_TUNE_RE = /fine.?tun(?:ing|e)|finetune|\.createFineTuningJob|openai\.fineTuning/i;
|
|
63
|
+
const PII_SCRUB_RE = /scrub|redact|anonymize|presidio|pii.?filter|removePii/i;
|
|
64
|
+
// ─── AI_FUNCTION_DESCRIPTION_USER_INPUT ───────────────────────────────────────
|
|
65
|
+
// Tool description field built from user-controlled data.
|
|
66
|
+
// Split into template-literal variant and string-concat variant.
|
|
67
|
+
const FUNC_DESC_TEMPLATE_RE = /description\s*:\s*`[^`]*\$\{(?:user|req\.|input\.|body\.)[^}]*\}/i;
|
|
68
|
+
const FUNC_DESC_CONCAT_RE = /description\s*:\s*['"][^'"]*['"]\s*\+\s*(?:user|req\.|input\.)/i;
|
|
69
|
+
// ─── AI_MISSING_CONTENT_FILTER ────────────────────────────────────────────────
|
|
70
|
+
const AI_OUTPUT_DIRECT_RE = /(?:completion|response|message)\.content.*res\.(?:json|send|write)/i;
|
|
71
|
+
const CONTENT_FILTER_RE = /moderate|moderation|content.?filter|openai\.moderations|guardrail|shield/i;
|
|
72
|
+
// ─── AI_INDIRECT_PROMPT_INJECTION ────────────────────────────────────────────
|
|
73
|
+
const EXTERNAL_FETCH_RE = /(?:fetch|axios\.get|got\.get|request\.get|nodemailer|imapflow|cheerio\.load|parseHTML|pdf\.extract|mammoth\.extract)/i;
|
|
74
|
+
const PROMPT_BUILD_NEARBY_RE = /(?:messages|prompt|systemPrompt|userMessage|content)\s*(?:=|\+=|\.push)/i;
|
|
75
|
+
// ─── AI_MARKDOWN_EXFIL_RISK ───────────────────────────────────────────────────
|
|
76
|
+
const MARKDOWN_RENDER_RE = /(?:dangerouslySetInnerHTML|innerHTML\s*=|marked\.parse|showdown|remark|unified)\s*\(/i;
|
|
77
|
+
// ─── AI_MEMORY_POISONING ──────────────────────────────────────────────────────
|
|
78
|
+
const MEMORY_WRITE_RE = /(?:memory\.add|memory\.save|memory\.set|memoryStore\.write|redis\.set)\s*\([^)]*(?:summary|context|history|assistant)/i;
|
|
79
|
+
// ─── AI_RAG_CORPUS_POISONING ──────────────────────────────────────────────────
|
|
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
|
+
// ─── AI_TOKEN_SMUGGLING ───────────────────────────────────────────────────────
|
|
82
|
+
const ZERO_WIDTH_RE = /\u200b|\u200c|\u200d|\u200e|\u200f|\u2060|\ufeff|\u202e|\u202f|\u2028|\u2029|\u00ad/;
|
|
83
|
+
// ─── AI_AGENTIC_PRIVILEGE_ESCALATION ─────────────────────────────────────────
|
|
84
|
+
const TOOL_REGISTER_RE = /(?:tools\.push|tools\.add|registerTool|addTool|extend_tools|capabilities\.push)\s*\([^)]*(?:response|output|completion|llm|agent)/i;
|
|
85
|
+
// ─── AI_LLM_JUDGE_MANIPULATION ───────────────────────────────────────────────
|
|
86
|
+
const LLM_JUDGE_RE = /(?:judge|evaluator|llm_eval|scoreWith|evaluate_with_llm|llmJudge|grader)\s*\(/i;
|
|
87
|
+
const JUDGE_USER_INPUT_RE = /(?:criteria|rubric|instruction)\s*[:=][^\n]*(?:userInput|req\.body|input\.|body\.)/i;
|
|
88
|
+
// ─── AI_IDOR_TOOL_CALLS ───────────────────────────────────────────────────────
|
|
89
|
+
const TOOL_IDOR_RE = /(?:toolCall|tool_call|toolHandler|function_call)\s*\([^)]*(?:args|arguments|params)\.[a-zA-Z]*[Ii][dD]/i;
|
|
90
|
+
const AUTHZ_CHECK_RE = /(?:authorize|checkPermission|hasAccess|enforceAuth|userId\s*===|ownedBy)/i;
|
|
91
|
+
// ─── AI_CONTEXT_STUFFING ──────────────────────────────────────────────────────
|
|
92
|
+
const INPUT_TOKEN_LIMIT_RE = /(?:maxTokens|max_tokens|tokenCount|countTokens|truncate.*tokens)/i;
|
|
93
|
+
// ─── AI_MULTIMODAL_INJECTION ──────────────────────────────────────────────────
|
|
94
|
+
const MULTIMODAL_RE = /(?:image_url|vision|file_content|image\/(?:jpeg|png|gif|webp)|application\/pdf|audio\/)/i;
|
|
95
|
+
const MESSAGES_ARRAY_RE = /messages\s*(?:=|\+=|\.push)\s*\[?/i;
|
|
96
|
+
// ─── AI_VECTOR_FILTER_BYPASS ──────────────────────────────────────────────────
|
|
97
|
+
const VECTOR_SOFT_FILTER_RE = /(?:similarity_search|vectorSearch|\.search\s*\()[^)]*(?:should|\$or|match_any)/i;
|
|
98
|
+
// ─── AI_STREAM_CHUNK_INJECTION ────────────────────────────────────────────────
|
|
99
|
+
const STREAM_FORWARD_RE = /(?:stream\.on\s*\(['"]data|for await.*chunk)[^\n]*(?:res\.write|socket\.send|push\(|emit\()/i;
|
|
100
|
+
const STREAM_VALIDATION_RE = /sanitize|validate|strip|encode|escape|DOMPurify/i;
|
|
101
|
+
// ─── AI_GENERATED_CODE_NO_AUDIT ───────────────────────────────────────────────
|
|
102
|
+
const AI_CODE_EXEC_RE = /(?:eval|exec|execSync|spawn|db\.query|prisma\.\$queryRaw|knex\.raw)\s*\([^)]*(?:response|completion|output|generated|llm|model)/i;
|
|
103
|
+
const AUDIT_LOG_RE = /audit(?:Log|log|\.log)|logger\.(?:info|warn|security)|logEvent|securityLog/i;
|
|
104
|
+
// ─── AI_EMBEDDING_INVERSION ───────────────────────────────────────────────────
|
|
105
|
+
const EMBEDDING_EXPOSE_RE = /(?:embedding|embeddings|vector)\.(?:data|values)\s*[,;)][^\n]*(?:res\.json|res\.send|JSON\.stringify|localStorage|log\s*\()/i;
|
|
106
|
+
// ─── Glob ignore list ─────────────────────────────────────────────────────────
|
|
107
|
+
const GLOB_IGNORE = [
|
|
108
|
+
"**/node_modules/**",
|
|
109
|
+
"**/.git/**",
|
|
110
|
+
"**/dist/**",
|
|
111
|
+
"**/fixtures/**",
|
|
112
|
+
"**/.mcp/**",
|
|
113
|
+
"**/.mcp/reviews/**",
|
|
114
|
+
"**/.mcp/reports/**"
|
|
115
|
+
];
|
|
116
|
+
function makeEvidence() {
|
|
117
|
+
return {
|
|
118
|
+
toolFiles: [],
|
|
119
|
+
injectionFiles: [],
|
|
120
|
+
promptConcatFiles: [],
|
|
121
|
+
evalOutputFiles: [],
|
|
122
|
+
piiPromptFiles: [],
|
|
123
|
+
rateLimitMissingFiles: [],
|
|
124
|
+
ragNoAuthzFiles: [],
|
|
125
|
+
toolArgsUnvalidatedFiles: [],
|
|
126
|
+
streamNoTimeoutFiles: [],
|
|
127
|
+
multiTenantLeakFiles: [],
|
|
128
|
+
agentUnboundedFiles: [],
|
|
129
|
+
hardcodedSecretPromptFiles: [],
|
|
130
|
+
modelUnpinnedFiles: [],
|
|
131
|
+
langchainDangerousFiles: [],
|
|
132
|
+
hfUnpinnedFiles: [],
|
|
133
|
+
vectorUnauthFiles: [],
|
|
134
|
+
fineTunePiiFiles: [],
|
|
135
|
+
funcDescUserInputFiles: [],
|
|
136
|
+
contentFilterMissingFiles: [],
|
|
137
|
+
indirectPromptInjectionFiles: [],
|
|
138
|
+
markdownExfilFiles: [],
|
|
139
|
+
memoryPoisoningFiles: [],
|
|
140
|
+
ragCorpusPoisoningFiles: [],
|
|
141
|
+
tokenSmugglingFiles: [],
|
|
142
|
+
agenticPrivEscFiles: [],
|
|
143
|
+
llmJudgeManipFiles: [],
|
|
144
|
+
idorToolCallFiles: [],
|
|
145
|
+
contextStuffingFiles: [],
|
|
146
|
+
multimodalInjectionFiles: [],
|
|
147
|
+
vectorFilterBypassFiles: [],
|
|
148
|
+
streamChunkInjectionFiles: [],
|
|
149
|
+
aiGeneratedCodeNoAuditFiles: [],
|
|
150
|
+
embeddingInversionFiles: [],
|
|
151
|
+
schemaDetected: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ─── Window-based context check ───────────────────────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* Returns true if `targetRe` matches within `windowSize` lines around any line
|
|
157
|
+
* matched by `anchorRe`. Used to detect paired patterns (e.g. search + authz).
|
|
158
|
+
*/
|
|
159
|
+
function windowMatch(lines, anchorRe, targetRe, windowSize) {
|
|
160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
161
|
+
if (!anchorRe.test(lines[i]))
|
|
33
162
|
continue;
|
|
163
|
+
const start = Math.max(0, i - windowSize);
|
|
164
|
+
const end = Math.min(lines.length - 1, i + windowSize);
|
|
165
|
+
for (let j = start; j <= end; j++) {
|
|
166
|
+
if (targetRe.test(lines[j]))
|
|
167
|
+
return true;
|
|
34
168
|
}
|
|
35
|
-
if (SCHEMA_RE.test(text)) {
|
|
36
|
-
schemaDetected = true;
|
|
37
|
-
}
|
|
38
|
-
if (TOOL_RE.test(text)) {
|
|
39
|
-
toolEvidence.push(file);
|
|
40
|
-
}
|
|
41
|
-
if (INJECTION_RE.test(text)) {
|
|
42
|
-
injectionEvidence.push(file);
|
|
43
|
-
}
|
|
44
169
|
}
|
|
45
|
-
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// ─── Scan helpers (split to keep cognitive complexity under threshold) ─────────
|
|
173
|
+
function scanExisting(file, text, ev) {
|
|
174
|
+
if (SCHEMA_RE.test(text))
|
|
175
|
+
ev.schemaDetected = true;
|
|
176
|
+
if (TOOL_RE.test(text))
|
|
177
|
+
ev.toolFiles.push(file);
|
|
178
|
+
if (INJECTION_RE.test(text))
|
|
179
|
+
ev.injectionFiles.push(file);
|
|
180
|
+
}
|
|
181
|
+
function scanPromptAndEval(file, text, ev) {
|
|
182
|
+
if (PROMPT_CONCAT_A_RE.test(text) || PROMPT_CONCAT_B_RE.test(text)) {
|
|
183
|
+
ev.promptConcatFiles.push(file);
|
|
184
|
+
}
|
|
185
|
+
if (OUTPUT_TO_EVAL_A_RE.test(text) || OUTPUT_TO_EVAL_B_RE.test(text)) {
|
|
186
|
+
ev.evalOutputFiles.push(file);
|
|
187
|
+
}
|
|
188
|
+
if (PII_IN_PROMPT_A_RE.test(text) || PII_IN_PROMPT_B_RE.test(text)) {
|
|
189
|
+
ev.piiPromptFiles.push(file);
|
|
190
|
+
}
|
|
191
|
+
if (FUNC_DESC_TEMPLATE_RE.test(text) || FUNC_DESC_CONCAT_RE.test(text)) {
|
|
192
|
+
ev.funcDescUserInputFiles.push(file);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function scanRateLimitAndContent(file, text, ev) {
|
|
196
|
+
const hasAiCall = AI_SDK_CALL_RE.test(text);
|
|
197
|
+
if (AI_ROUTE_HANDLER_RE.test(text) && hasAiCall && !RATE_LIMIT_PRESENT_RE.test(text)) {
|
|
198
|
+
ev.rateLimitMissingFiles.push(file);
|
|
199
|
+
}
|
|
200
|
+
if (hasAiCall && AI_OUTPUT_DIRECT_RE.test(text) && !CONTENT_FILTER_RE.test(text)) {
|
|
201
|
+
ev.contentFilterMissingFiles.push(file);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function scanContextAndLoop(file, text, lines, ev) {
|
|
205
|
+
if (VECTOR_SEARCH_RE.test(text) && !windowMatch(lines, VECTOR_SEARCH_RE, AUTHZ_NEARBY_RE, 10)) {
|
|
206
|
+
ev.ragNoAuthzFiles.push(file);
|
|
207
|
+
}
|
|
208
|
+
if (TOOL_ARGS_HANDLER_RE.test(text) && !TOOL_ARGS_VALIDATED_RE.test(text)) {
|
|
209
|
+
ev.toolArgsUnvalidatedFiles.push(file);
|
|
210
|
+
}
|
|
211
|
+
if (STREAMING_CALL_RE.test(text) && !windowMatch(lines, STREAMING_CALL_RE, STREAM_SAFEGUARD_RE, 20)) {
|
|
212
|
+
ev.streamNoTimeoutFiles.push(file);
|
|
213
|
+
}
|
|
214
|
+
if (CONTEXT_BUILD_RE.test(text) && !windowMatch(lines, CONTEXT_BUILD_RE, TENANT_FILTER_RE, 15)) {
|
|
215
|
+
ev.multiTenantLeakFiles.push(file);
|
|
216
|
+
}
|
|
217
|
+
if (AGENT_LOOP_RE.test(text) && !windowMatch(lines, AGENT_LOOP_RE, ITERATION_CAP_RE, 10)) {
|
|
218
|
+
ev.agentUnboundedFiles.push(file);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function scanModelsAndSupply(file, text, lines, ev) {
|
|
222
|
+
if (HARDCODED_SECRET_A_RE.test(text) || HARDCODED_SECRET_B_RE.test(text)) {
|
|
223
|
+
ev.hardcodedSecretPromptFiles.push(file);
|
|
224
|
+
}
|
|
225
|
+
if (MODEL_UNPINNED_RE.test(text) && !MODEL_PINNED_RE.test(text)) {
|
|
226
|
+
ev.modelUnpinnedFiles.push(file);
|
|
227
|
+
}
|
|
228
|
+
if (LANGCHAIN_DANGEROUS_RE.test(text)) {
|
|
229
|
+
ev.langchainDangerousFiles.push(file);
|
|
230
|
+
}
|
|
231
|
+
if (HF_PRETRAINED_RE.test(text) && !windowMatch(lines, HF_PRETRAINED_RE, HF_REVISION_RE, 5)) {
|
|
232
|
+
ev.hfUnpinnedFiles.push(file);
|
|
233
|
+
}
|
|
234
|
+
if (VECTOR_CLIENT_RE.test(text) && !windowMatch(lines, VECTOR_CLIENT_RE, VECTOR_AUTH_RE, 10)) {
|
|
235
|
+
ev.vectorUnauthFiles.push(file);
|
|
236
|
+
}
|
|
237
|
+
if (FINE_TUNE_RE.test(text) && !PII_SCRUB_RE.test(text)) {
|
|
238
|
+
ev.fineTunePiiFiles.push(file);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function scanNewAiThreats(file, text, lines, ev) {
|
|
242
|
+
// AI_INDIRECT_PROMPT_INJECTION: external data fetch near prompt construction
|
|
243
|
+
if (EXTERNAL_FETCH_RE.test(text) && windowMatch(lines, EXTERNAL_FETCH_RE, PROMPT_BUILD_NEARBY_RE, 20)) {
|
|
244
|
+
ev.indirectPromptInjectionFiles.push(file);
|
|
245
|
+
}
|
|
246
|
+
// AI_MARKDOWN_EXFIL_RISK: markdown renderer called on LLM output variables
|
|
247
|
+
if (MARKDOWN_RENDER_RE.test(text) && AI_SDK_CALL_RE.test(text)) {
|
|
248
|
+
ev.markdownExfilFiles.push(file);
|
|
249
|
+
}
|
|
250
|
+
// AI_MEMORY_POISONING: unsanitized data written to memory/session store
|
|
251
|
+
if (MEMORY_WRITE_RE.test(text)) {
|
|
252
|
+
ev.memoryPoisoningFiles.push(file);
|
|
253
|
+
}
|
|
254
|
+
// AI_RAG_CORPUS_POISONING: user-supplied data upserted directly into vector store
|
|
255
|
+
if (VECTOR_UPSERT_RE.test(text)) {
|
|
256
|
+
ev.ragCorpusPoisoningFiles.push(file);
|
|
257
|
+
}
|
|
258
|
+
// AI_TOKEN_SMUGGLING: zero-width / invisible Unicode characters in source
|
|
259
|
+
if (ZERO_WIDTH_RE.test(text)) {
|
|
260
|
+
ev.tokenSmugglingFiles.push(file);
|
|
261
|
+
}
|
|
262
|
+
// AI_AGENTIC_PRIVILEGE_ESCALATION: tools registered from LLM output
|
|
263
|
+
if (TOOL_REGISTER_RE.test(text)) {
|
|
264
|
+
ev.agenticPrivEscFiles.push(file);
|
|
265
|
+
}
|
|
266
|
+
// AI_LLM_JUDGE_MANIPULATION: LLM judge with user-controlled criteria/rubric
|
|
267
|
+
if (LLM_JUDGE_RE.test(text) && JUDGE_USER_INPUT_RE.test(text)) {
|
|
268
|
+
ev.llmJudgeManipFiles.push(file);
|
|
269
|
+
}
|
|
270
|
+
// AI_IDOR_TOOL_CALLS: tool handler resolves an ID from args without authz check
|
|
271
|
+
if (TOOL_IDOR_RE.test(text) && !windowMatch(lines, TOOL_IDOR_RE, AUTHZ_CHECK_RE, 15)) {
|
|
272
|
+
ev.idorToolCallFiles.push(file);
|
|
273
|
+
}
|
|
274
|
+
// AI_CONTEXT_STUFFING: AI SDK call with no token limit / truncation nearby
|
|
275
|
+
if (AI_SDK_CALL_RE.test(text) && !windowMatch(lines, AI_SDK_CALL_RE, INPUT_TOKEN_LIMIT_RE, 20)) {
|
|
276
|
+
ev.contextStuffingFiles.push(file);
|
|
277
|
+
}
|
|
278
|
+
// AI_MULTIMODAL_INJECTION: multimodal content fed directly into messages array
|
|
279
|
+
if (MULTIMODAL_RE.test(text) && windowMatch(lines, MULTIMODAL_RE, MESSAGES_ARRAY_RE, 10)) {
|
|
280
|
+
ev.multimodalInjectionFiles.push(file);
|
|
281
|
+
}
|
|
282
|
+
// AI_VECTOR_FILTER_BYPASS: soft/optional vector filter without hard tenant guard
|
|
283
|
+
if (VECTOR_SOFT_FILTER_RE.test(text)) {
|
|
284
|
+
ev.vectorFilterBypassFiles.push(file);
|
|
285
|
+
}
|
|
286
|
+
// AI_STREAM_CHUNK_INJECTION: stream chunks forwarded to client without sanitization
|
|
287
|
+
if (STREAM_FORWARD_RE.test(text) && !windowMatch(lines, STREAM_FORWARD_RE, STREAM_VALIDATION_RE, 10)) {
|
|
288
|
+
ev.streamChunkInjectionFiles.push(file);
|
|
289
|
+
}
|
|
290
|
+
// AI_GENERATED_CODE_NO_AUDIT: AI-generated code executed without audit log
|
|
291
|
+
if (AI_CODE_EXEC_RE.test(text) && !windowMatch(lines, AI_CODE_EXEC_RE, AUDIT_LOG_RE, 15)) {
|
|
292
|
+
ev.aiGeneratedCodeNoAuditFiles.push(file);
|
|
293
|
+
}
|
|
294
|
+
// AI_EMBEDDING_INVERSION: raw embeddings/vectors serialised in API response or logs
|
|
295
|
+
if (EMBEDDING_EXPOSE_RE.test(text)) {
|
|
296
|
+
ev.embeddingInversionFiles.push(file);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Single-pass per-file scanner — delegates to focused helpers. */
|
|
300
|
+
function scanFile(file, text, ev) {
|
|
301
|
+
const lines = text.split("\n");
|
|
302
|
+
scanExisting(file, text, ev);
|
|
303
|
+
scanPromptAndEval(file, text, ev);
|
|
304
|
+
scanRateLimitAndContent(file, text, ev);
|
|
305
|
+
scanContextAndLoop(file, text, lines, ev);
|
|
306
|
+
scanModelsAndSupply(file, text, lines, ev);
|
|
307
|
+
scanNewAiThreats(file, text, lines, ev);
|
|
308
|
+
}
|
|
309
|
+
// ─── Finding builders (split to keep checkAi cognitive complexity low) ─────────
|
|
310
|
+
function buildBaseFindings(ev, findings) {
|
|
311
|
+
if (ev.toolFiles.length > 0 && !ev.schemaDetected) {
|
|
46
312
|
findings.push({
|
|
47
313
|
id: "AI_OUTPUT_BOUNDS_MISSING",
|
|
48
314
|
title: "AI/tooling present but bounded output (schema validation) not detected",
|
|
49
315
|
severity: "HIGH",
|
|
50
|
-
evidence:
|
|
316
|
+
evidence: ev.toolFiles,
|
|
51
317
|
requiredActions: [
|
|
52
318
|
"Enforce bounded outputs via JSON schema validation for every AI response used by code.",
|
|
53
319
|
"Add prompt-injection defenses: input sanitization, tool allowlists, deny-by-default tool router, and sensitive data redaction."
|
|
54
320
|
]
|
|
55
321
|
});
|
|
56
322
|
}
|
|
57
|
-
if (
|
|
323
|
+
if (ev.injectionFiles.length > 0) {
|
|
58
324
|
findings.push({
|
|
59
325
|
id: "AI_INJECTION_CUES",
|
|
60
326
|
title: "Potential prompt injection cues detected. Requires explicit mitigations and tests.",
|
|
61
327
|
severity: "MEDIUM",
|
|
62
|
-
evidence:
|
|
328
|
+
evidence: ev.injectionFiles,
|
|
63
329
|
requiredActions: [
|
|
64
330
|
"Add multi-layer prompt-injection protection: instruction hierarchy enforcement, content isolation, tool gating, and output validation.",
|
|
65
331
|
"Add a red-team test harness with injection payloads and exfil attempts."
|
|
66
332
|
]
|
|
67
333
|
});
|
|
68
334
|
}
|
|
335
|
+
}
|
|
336
|
+
function buildPromptAndEvalFindings(ev, findings) {
|
|
337
|
+
if (ev.promptConcatFiles.length > 0) {
|
|
338
|
+
findings.push({
|
|
339
|
+
id: "AI_PROMPT_CONCAT",
|
|
340
|
+
title: "System prompt constructed by string concatenation with user-controlled input",
|
|
341
|
+
severity: "CRITICAL",
|
|
342
|
+
evidence: ev.promptConcatFiles,
|
|
343
|
+
requiredActions: [
|
|
344
|
+
"Never concatenate user input directly into the system prompt. Use a strict template with clearly delimited user sections.",
|
|
345
|
+
"Sanitize and escape user-supplied content before embedding it anywhere in the prompt.",
|
|
346
|
+
"Implement instruction-hierarchy enforcement so user content cannot override system instructions."
|
|
347
|
+
]
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (ev.evalOutputFiles.length > 0) {
|
|
351
|
+
findings.push({
|
|
352
|
+
id: "AI_OUTPUT_TO_EVAL",
|
|
353
|
+
title: "LLM output passed to eval(), exec(), or spawn() — arbitrary code execution risk",
|
|
354
|
+
severity: "CRITICAL",
|
|
355
|
+
evidence: ev.evalOutputFiles,
|
|
356
|
+
requiredActions: [
|
|
357
|
+
"Never pass AI-generated text to eval(), exec(), or spawn(). Parse structured output instead.",
|
|
358
|
+
"If code execution from AI output is required, use a sandboxed execution environment (e2b, Firecracker, WASM) with strict allow-lists.",
|
|
359
|
+
"Validate and parse AI output through a strict JSON schema before any programmatic use."
|
|
360
|
+
]
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (ev.piiPromptFiles.length > 0) {
|
|
364
|
+
findings.push({
|
|
365
|
+
id: "AI_PII_IN_PROMPT",
|
|
366
|
+
title: "PII field names detected inside AI prompt template literals",
|
|
367
|
+
severity: "HIGH",
|
|
368
|
+
evidence: ev.piiPromptFiles,
|
|
369
|
+
requiredActions: [
|
|
370
|
+
"Remove PII (SSN, card numbers, CVV, passwords) from prompt templates. Pass only anonymized or tokenized references.",
|
|
371
|
+
"Implement a PII scrubber (e.g., Microsoft Presidio) at the prompt construction boundary.",
|
|
372
|
+
"Audit all prompt templates for data minimization compliance (GDPR Art. 5, PCI DSS Req. 3)."
|
|
373
|
+
]
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (ev.funcDescUserInputFiles.length > 0) {
|
|
377
|
+
findings.push({
|
|
378
|
+
id: "AI_FUNCTION_DESCRIPTION_USER_INPUT",
|
|
379
|
+
title: "Tool/function schema 'description' field constructed from user-controlled input",
|
|
380
|
+
severity: "HIGH",
|
|
381
|
+
evidence: ev.funcDescUserInputFiles,
|
|
382
|
+
requiredActions: [
|
|
383
|
+
"Never embed user-supplied strings in tool or function description fields — this enables prompt injection via schema poisoning.",
|
|
384
|
+
"Define tool descriptions as static compile-time constants only.",
|
|
385
|
+
"Validate that tool schemas are constructed from trusted, server-controlled values before sending to the LLM."
|
|
386
|
+
]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function buildAccessFindings(ev, findings) {
|
|
391
|
+
if (ev.rateLimitMissingFiles.length > 0) {
|
|
392
|
+
findings.push({
|
|
393
|
+
id: "AI_RATE_LIMIT_MISSING",
|
|
394
|
+
title: "AI API route handlers detected without rate limiting middleware",
|
|
395
|
+
severity: "HIGH",
|
|
396
|
+
evidence: ev.rateLimitMissingFiles,
|
|
397
|
+
requiredActions: [
|
|
398
|
+
"Apply rate limiting (e.g., express-rate-limit, Upstash Ratelimit) to every route that triggers an LLM call.",
|
|
399
|
+
"Set per-user and per-IP token budgets to prevent abuse and runaway inference costs.",
|
|
400
|
+
"Add alerting when per-user token consumption exceeds defined thresholds."
|
|
401
|
+
]
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (ev.ragNoAuthzFiles.length > 0) {
|
|
405
|
+
findings.push({
|
|
406
|
+
id: "AI_RAG_AUTHZ_MISSING",
|
|
407
|
+
title: "Vector / similarity search results used without authorization check",
|
|
408
|
+
severity: "HIGH",
|
|
409
|
+
evidence: ev.ragNoAuthzFiles,
|
|
410
|
+
requiredActions: [
|
|
411
|
+
"Filter vector search results by userId/tenantId before passing them to the LLM context.",
|
|
412
|
+
"Apply row-level security or namespace isolation in your vector database (Pinecone namespaces, Qdrant payload filters).",
|
|
413
|
+
"Never inject retrieved documents into the prompt without confirming the requesting user has read access to those documents."
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (ev.toolArgsUnvalidatedFiles.length > 0) {
|
|
418
|
+
findings.push({
|
|
419
|
+
id: "AI_TOOL_ARGS_UNVALIDATED",
|
|
420
|
+
title: "Tool/function call arguments handled without schema validation",
|
|
421
|
+
severity: "HIGH",
|
|
422
|
+
evidence: ev.toolArgsUnvalidatedFiles,
|
|
423
|
+
requiredActions: [
|
|
424
|
+
"Validate every tool-call argument object through a Zod schema (z.parse) before executing the tool.",
|
|
425
|
+
"Reject or throw on unexpected keys — use z.object().strict() to disallow additional properties.",
|
|
426
|
+
"Log validation failures as security events; repeated failures may indicate adversarial tool-call injection."
|
|
427
|
+
]
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (ev.multiTenantLeakFiles.length > 0) {
|
|
431
|
+
findings.push({
|
|
432
|
+
id: "AI_MULTI_TENANT_CONTEXT_LEAK",
|
|
433
|
+
title: "LLM conversation context / history built without tenant isolation",
|
|
434
|
+
severity: "CRITICAL",
|
|
435
|
+
evidence: ev.multiTenantLeakFiles,
|
|
436
|
+
requiredActions: [
|
|
437
|
+
"Filter all history and context arrays by userId/tenantId/orgId before building the prompt.",
|
|
438
|
+
"Store conversation history in tenant-scoped keys (e.g., Redis key prefix includes tenantId).",
|
|
439
|
+
"Add an integration test that verifies cross-tenant context cannot bleed between requests."
|
|
440
|
+
]
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function buildRuntimeFindings(ev, findings) {
|
|
445
|
+
if (ev.streamNoTimeoutFiles.length > 0) {
|
|
446
|
+
findings.push({
|
|
447
|
+
id: "AI_STREAMING_NO_TIMEOUT",
|
|
448
|
+
title: "Streaming LLM calls detected without AbortController or timeout",
|
|
449
|
+
severity: "MEDIUM",
|
|
450
|
+
evidence: ev.streamNoTimeoutFiles,
|
|
451
|
+
requiredActions: [
|
|
452
|
+
"Attach an AbortController signal to every streaming call: pass `signal: controller.signal` and call controller.abort() after a timeout.",
|
|
453
|
+
"Set a maximum stream duration (e.g., 60 s) and enforce it server-side to prevent resource exhaustion.",
|
|
454
|
+
"Ensure clients handle stream interruptions gracefully without leaking connections or memory."
|
|
455
|
+
]
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (ev.agentUnboundedFiles.length > 0) {
|
|
459
|
+
findings.push({
|
|
460
|
+
id: "AI_AGENT_UNBOUNDED_LOOP",
|
|
461
|
+
title: "Agentic loop detected without a maximum iteration / step limit",
|
|
462
|
+
severity: "HIGH",
|
|
463
|
+
evidence: ev.agentUnboundedFiles,
|
|
464
|
+
requiredActions: [
|
|
465
|
+
"Define a MAX_ITERATIONS constant and break or throw when the limit is exceeded.",
|
|
466
|
+
"Track cumulative token consumption across iterations and enforce a hard budget.",
|
|
467
|
+
"Log iteration counts and alert when agents consistently approach the limit (may indicate prompt logic errors)."
|
|
468
|
+
]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (ev.contentFilterMissingFiles.length > 0) {
|
|
472
|
+
findings.push({
|
|
473
|
+
id: "AI_MISSING_CONTENT_FILTER",
|
|
474
|
+
title: "LLM output sent directly to client without content moderation or filtering",
|
|
475
|
+
severity: "MEDIUM",
|
|
476
|
+
evidence: ev.contentFilterMissingFiles,
|
|
477
|
+
requiredActions: [
|
|
478
|
+
"Pass all LLM outputs through a moderation layer (openai.moderations.create, Anthropic's safety guidelines, or a custom classifier) before returning to the client.",
|
|
479
|
+
"Define and enforce an output policy: refuse, truncate, or warn on policy-violating content.",
|
|
480
|
+
"Log moderation decisions (without PII) for audit and model safety feedback purposes."
|
|
481
|
+
]
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function buildSupplyChainFindings(ev, findings) {
|
|
486
|
+
if (ev.hardcodedSecretPromptFiles.length > 0) {
|
|
487
|
+
findings.push({
|
|
488
|
+
id: "AI_SYSTEM_PROMPT_HARDCODED",
|
|
489
|
+
title: "Hardcoded secrets or credentials detected inside system prompt template literals",
|
|
490
|
+
severity: "MEDIUM",
|
|
491
|
+
evidence: ev.hardcodedSecretPromptFiles,
|
|
492
|
+
requiredActions: [
|
|
493
|
+
"Move all secrets to environment variables and inject them at runtime — never embed in source.",
|
|
494
|
+
"Rotate any credentials that may have been exposed via the prompt (they may appear in LLM logs or responses).",
|
|
495
|
+
"Run a secret-scanning pre-commit hook (e.g., gitleaks, truffleHog) to catch future occurrences."
|
|
496
|
+
]
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (ev.modelUnpinnedFiles.length > 0) {
|
|
500
|
+
findings.push({
|
|
501
|
+
id: "AI_MODEL_VERSION_UNPINNED",
|
|
502
|
+
title: "AI model identifier not pinned to a specific version/date suffix",
|
|
503
|
+
severity: "MEDIUM",
|
|
504
|
+
evidence: ev.modelUnpinnedFiles,
|
|
505
|
+
requiredActions: [
|
|
506
|
+
"Pin model versions with their date suffix (e.g., gpt-4-0125-preview, claude-sonnet-4-6) to ensure reproducible behavior.",
|
|
507
|
+
"Treat model upgrades as a dependency change: test, review, and deploy deliberately — not automatically.",
|
|
508
|
+
"Add a CI check that rejects unpinned model strings in AI SDK calls."
|
|
509
|
+
]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (ev.langchainDangerousFiles.length > 0) {
|
|
513
|
+
findings.push({
|
|
514
|
+
id: "AI_LANGCHAIN_DANGEROUS_TOOLS",
|
|
515
|
+
title: "LangChain code-execution tool detected (PythonREPLTool / BashTool / ShellTool)",
|
|
516
|
+
severity: "CRITICAL",
|
|
517
|
+
evidence: ev.langchainDangerousFiles,
|
|
518
|
+
requiredActions: [
|
|
519
|
+
"Remove PythonREPLTool, BashTool, and ShellTool from all production agents — they allow arbitrary OS-level code execution.",
|
|
520
|
+
"If code execution is required, use an isolated sandbox (e2b, Modal, AWS Lambda) with network egress restrictions and no filesystem write access.",
|
|
521
|
+
"Implement a tool allow-list; any tool not explicitly listed should be denied by default."
|
|
522
|
+
]
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
if (ev.hfUnpinnedFiles.length > 0) {
|
|
526
|
+
findings.push({
|
|
527
|
+
id: "AI_HUGGINGFACE_UNPINNED",
|
|
528
|
+
title: "HuggingFace model loaded via from_pretrained() without a revision= pin",
|
|
529
|
+
severity: "HIGH",
|
|
530
|
+
evidence: ev.hfUnpinnedFiles,
|
|
531
|
+
requiredActions: [
|
|
532
|
+
"Always specify `revision='<commit-sha>'` in from_pretrained() to pin the exact model weights.",
|
|
533
|
+
"Validate model checksums (SHA256) after download before loading into memory.",
|
|
534
|
+
"Use private model registries or mirrored repositories for production workloads — never load directly from the public Hub without pinning."
|
|
535
|
+
]
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (ev.vectorUnauthFiles.length > 0) {
|
|
539
|
+
findings.push({
|
|
540
|
+
id: "AI_EMBEDDING_UNAUTH",
|
|
541
|
+
title: "Vector database client initialized without authentication credentials",
|
|
542
|
+
severity: "HIGH",
|
|
543
|
+
evidence: ev.vectorUnauthFiles,
|
|
544
|
+
requiredActions: [
|
|
545
|
+
"Pass api_key / auth_token when constructing the vector DB client — never use anonymous or open access in non-local environments.",
|
|
546
|
+
"Store vector DB credentials in a secrets manager (AWS Secrets Manager, Vault) and inject at runtime.",
|
|
547
|
+
"Enable network-level restrictions (VPC, IP allow-list) in addition to API key authentication."
|
|
548
|
+
]
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (ev.fineTunePiiFiles.length > 0) {
|
|
552
|
+
findings.push({
|
|
553
|
+
id: "AI_FINE_TUNE_DATA_PII",
|
|
554
|
+
title: "Fine-tuning data pipeline detected without PII scrubbing step",
|
|
555
|
+
severity: "HIGH",
|
|
556
|
+
evidence: ev.fineTunePiiFiles,
|
|
557
|
+
requiredActions: [
|
|
558
|
+
"Run all fine-tuning training data through a PII detection and redaction pipeline (e.g., Microsoft Presidio, AWS Comprehend) before submission.",
|
|
559
|
+
"Maintain an audit log of what data was used to train each model version.",
|
|
560
|
+
"Review the model provider's data retention policy — submitted fine-tune data may be stored by the provider."
|
|
561
|
+
]
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function buildNewAiThreatFindings(ev, findings) {
|
|
566
|
+
if (ev.indirectPromptInjectionFiles.length > 0) {
|
|
567
|
+
findings.push({
|
|
568
|
+
id: "AI_INDIRECT_PROMPT_INJECTION",
|
|
569
|
+
title: "External data fetched and inserted into LLM prompt without sanitization — indirect prompt injection risk",
|
|
570
|
+
severity: "CRITICAL",
|
|
571
|
+
evidence: ev.indirectPromptInjectionFiles,
|
|
572
|
+
requiredActions: [
|
|
573
|
+
"Treat all externally fetched content (web pages, emails, PDFs, APIs) as untrusted — sanitize and delimit it before inserting into any prompt (CWE-77, MITRE ATLAS AML.T0051).",
|
|
574
|
+
"Use clearly-marked content boundaries (e.g., XML tags or structured separators) so the model can distinguish instructions from data.",
|
|
575
|
+
"Apply an LLM-input firewall or content-isolation layer that strips control-plane instructions from user-sourced text before prompt construction."
|
|
576
|
+
]
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (ev.markdownExfilFiles.length > 0) {
|
|
580
|
+
findings.push({
|
|
581
|
+
id: "AI_MARKDOWN_EXFIL_RISK",
|
|
582
|
+
title: "LLM output rendered as Markdown/HTML without sanitization — data exfiltration via link injection risk",
|
|
583
|
+
severity: "CRITICAL",
|
|
584
|
+
evidence: ev.markdownExfilFiles,
|
|
585
|
+
requiredActions: [
|
|
586
|
+
"Sanitize all LLM-generated Markdown/HTML with a strict allowlist renderer (e.g., DOMPurify) before rendering client-side (CWE-79, MITRE ATLAS AML.T0054).",
|
|
587
|
+
"Disable auto-link and image rendering in the Markdown parser — these are the primary exfiltration vectors.",
|
|
588
|
+
"Apply a Content-Security-Policy that blocks external image/script loads to prevent pixel-tracking and data exfiltration even if sanitization is bypassed."
|
|
589
|
+
]
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (ev.memoryPoisoningFiles.length > 0) {
|
|
593
|
+
findings.push({
|
|
594
|
+
id: "AI_MEMORY_POISONING",
|
|
595
|
+
title: "Data written to agent memory store without validation — memory poisoning risk",
|
|
596
|
+
severity: "CRITICAL",
|
|
597
|
+
evidence: ev.memoryPoisoningFiles,
|
|
598
|
+
requiredActions: [
|
|
599
|
+
"Validate and sanitize all content before persisting to memory/session stores — attacker-controlled summaries can shape future model behavior (MITRE ATLAS AML.T0051.000, CWE-20).",
|
|
600
|
+
"Apply per-user memory namespacing and enforce write authorization so one user cannot poison another's context.",
|
|
601
|
+
"Implement memory integrity checks: hash-and-verify stored entries on read; alert on unexpected modifications."
|
|
602
|
+
]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (ev.ragCorpusPoisoningFiles.length > 0) {
|
|
606
|
+
findings.push({
|
|
607
|
+
id: "AI_RAG_CORPUS_POISONING",
|
|
608
|
+
title: "User-supplied content upserted directly into vector store — RAG corpus poisoning risk",
|
|
609
|
+
severity: "HIGH",
|
|
610
|
+
evidence: ev.ragCorpusPoisoningFiles,
|
|
611
|
+
requiredActions: [
|
|
612
|
+
"Never index user-uploaded content without human or automated review — poisoned documents retrieved at query time can hijack agent behavior (MITRE ATLAS AML.T0020, CWE-349).",
|
|
613
|
+
"Quarantine and scan ingested documents through a moderation pipeline before they are made retrievable.",
|
|
614
|
+
"Isolate tenant corpora using vector DB namespaces or collection-level ACLs to prevent cross-tenant retrieval poisoning."
|
|
615
|
+
]
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (ev.tokenSmugglingFiles.length > 0) {
|
|
619
|
+
findings.push({
|
|
620
|
+
id: "AI_TOKEN_SMUGGLING",
|
|
621
|
+
title: "Zero-width or invisible Unicode characters detected in source files — token smuggling risk",
|
|
622
|
+
severity: "HIGH",
|
|
623
|
+
evidence: ev.tokenSmugglingFiles,
|
|
624
|
+
requiredActions: [
|
|
625
|
+
"Audit all source files containing zero-width characters (U+200B–U+200F, U+2060, U+FEFF, U+202E–U+202F, U+2028–U+2029, U+00AD) — these can encode hidden instructions invisible to reviewers (CWE-116, MITRE ATLAS AML.T0051).",
|
|
626
|
+
"Add a pre-commit hook (e.g., rg '[\\u200b-\\u200f\\u2060\\ufeff\\u202e\\u202f\\u2028\\u2029\\u00ad]') that rejects files containing homoglyph/zero-width characters.",
|
|
627
|
+
"Normalize all user-supplied strings via Unicode NFC + strip non-printable characters before tokenization or storage."
|
|
628
|
+
]
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (ev.agenticPrivEscFiles.length > 0) {
|
|
632
|
+
findings.push({
|
|
633
|
+
id: "AI_AGENTIC_PRIVILEGE_ESCALATION",
|
|
634
|
+
title: "Agent tool registry modified from LLM output — privilege escalation via tool injection risk",
|
|
635
|
+
severity: "CRITICAL",
|
|
636
|
+
evidence: ev.agenticPrivEscFiles,
|
|
637
|
+
requiredActions: [
|
|
638
|
+
"Never register tools from LLM completions, API responses, or any runtime-generated data — tool definitions must be static and code-reviewed (MITRE ATLAS AML.T0054, CWE-284).",
|
|
639
|
+
"Enforce a tool allowlist at startup; reject any attempt to add, modify, or extend tools at runtime.",
|
|
640
|
+
"Apply principle of least privilege to all registered tools — each tool should have only the permissions required for its declared function."
|
|
641
|
+
]
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (ev.llmJudgeManipFiles.length > 0) {
|
|
645
|
+
findings.push({
|
|
646
|
+
id: "AI_LLM_JUDGE_MANIPULATION",
|
|
647
|
+
title: "LLM-as-judge evaluator accepts user-controlled criteria or rubric — judge manipulation risk",
|
|
648
|
+
severity: "HIGH",
|
|
649
|
+
evidence: ev.llmJudgeManipFiles,
|
|
650
|
+
requiredActions: [
|
|
651
|
+
"Define evaluation criteria and rubrics as static, server-controlled constants — never interpolate user input into judge instructions (MITRE ATLAS AML.T0051, CWE-77).",
|
|
652
|
+
"Run LLM judges in a separate trust domain with no access to production tools or data stores.",
|
|
653
|
+
"Log all judge inputs and outputs for audit; flag evaluations where the criteria field contains unusual formatting or injection-like patterns."
|
|
654
|
+
]
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (ev.idorToolCallFiles.length > 0) {
|
|
658
|
+
findings.push({
|
|
659
|
+
id: "AI_IDOR_TOOL_CALLS",
|
|
660
|
+
title: "Tool call handler resolves a resource ID from arguments without authorization check — IDOR risk",
|
|
661
|
+
severity: "CRITICAL",
|
|
662
|
+
evidence: ev.idorToolCallFiles,
|
|
663
|
+
requiredActions: [
|
|
664
|
+
"Enforce ownership/authorization checks on every ID extracted from tool call arguments before accessing the resource (CWE-639, MITRE ATLAS AML.T0054).",
|
|
665
|
+
"Never rely on the LLM to supply or validate IDs for sensitive operations — resolve them from the authenticated session context instead.",
|
|
666
|
+
"Apply object-level authorization (OLA) middleware that binds resource IDs to the requesting user's identity before tool execution."
|
|
667
|
+
]
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (ev.contextStuffingFiles.length > 0) {
|
|
671
|
+
findings.push({
|
|
672
|
+
id: "AI_CONTEXT_STUFFING",
|
|
673
|
+
title: "AI SDK call detected without input token limit or truncation — context stuffing / cost-exhaustion risk",
|
|
674
|
+
severity: "HIGH",
|
|
675
|
+
evidence: ev.contextStuffingFiles,
|
|
676
|
+
requiredActions: [
|
|
677
|
+
"Enforce a maxTokens / max_tokens cap on every LLM call and truncate inputs that exceed the budget before sending (CWE-400, MITRE ATLAS AML.T0057).",
|
|
678
|
+
"Count tokens client-side before submission using tiktoken or the provider's token-counting API and reject oversized inputs early.",
|
|
679
|
+
"Set per-user and per-session token budgets with alerting to detect context-stuffing abuse patterns."
|
|
680
|
+
]
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (ev.multimodalInjectionFiles.length > 0) {
|
|
684
|
+
findings.push({
|
|
685
|
+
id: "AI_MULTIMODAL_INJECTION",
|
|
686
|
+
title: "Multimodal content (image/PDF/audio) fed into messages array — multimodal prompt injection risk",
|
|
687
|
+
severity: "CRITICAL",
|
|
688
|
+
evidence: ev.multimodalInjectionFiles,
|
|
689
|
+
requiredActions: [
|
|
690
|
+
"Validate and sanitize all multimodal inputs before including them in the messages array — images and PDFs can encode hidden text instructions that override system prompts (MITRE ATLAS AML.T0051, CWE-20).",
|
|
691
|
+
"Apply file-type verification (magic bytes, not extension) and size limits to all uploaded multimodal assets before forwarding to the LLM.",
|
|
692
|
+
"Consider a two-stage pipeline: extract text from multimodal content first, sanitize the extracted text, then pass as clearly delimited user content."
|
|
693
|
+
]
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (ev.vectorFilterBypassFiles.length > 0) {
|
|
697
|
+
findings.push({
|
|
698
|
+
id: "AI_VECTOR_FILTER_BYPASS",
|
|
699
|
+
title: "Vector search uses soft/optional filter (should/$or/match_any) — filter bypass and cross-tenant data leak risk",
|
|
700
|
+
severity: "HIGH",
|
|
701
|
+
evidence: ev.vectorFilterBypassFiles,
|
|
702
|
+
requiredActions: [
|
|
703
|
+
"Replace soft/optional filters (should, $or, match_any) with hard mandatory filters (must, $and, match_all) for tenant and ownership constraints in all vector searches (CWE-285, MITRE ATLAS AML.T0025).",
|
|
704
|
+
"Test that vector search results never return documents outside the authenticated user's namespace even under adversarial query conditions.",
|
|
705
|
+
"Apply defense-in-depth: combine vector DB ACLs with application-layer result filtering before injecting retrieved documents into the prompt."
|
|
706
|
+
]
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (ev.streamChunkInjectionFiles.length > 0) {
|
|
710
|
+
findings.push({
|
|
711
|
+
id: "AI_STREAM_CHUNK_INJECTION",
|
|
712
|
+
title: "LLM stream chunks forwarded to client without validation — stream chunk injection risk",
|
|
713
|
+
severity: "HIGH",
|
|
714
|
+
evidence: ev.streamChunkInjectionFiles,
|
|
715
|
+
requiredActions: [
|
|
716
|
+
"Validate and sanitize each stream chunk before forwarding to the client — malicious chunks can contain XSS payloads, HTML injection, or embedded instructions (CWE-79, MITRE ATLAS AML.T0054).",
|
|
717
|
+
"Apply an incremental sanitizer (e.g., streaming DOMPurify or a custom strip function) on the server-side stream pipeline.",
|
|
718
|
+
"Add a maximum chunk-rate limit and total-response-size cap to prevent stream-based resource exhaustion."
|
|
719
|
+
]
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
if (ev.aiGeneratedCodeNoAuditFiles.length > 0) {
|
|
723
|
+
findings.push({
|
|
724
|
+
id: "AI_GENERATED_CODE_NO_AUDIT",
|
|
725
|
+
title: "AI-generated or LLM-completion output passed to code execution without audit logging",
|
|
726
|
+
severity: "HIGH",
|
|
727
|
+
evidence: ev.aiGeneratedCodeNoAuditFiles,
|
|
728
|
+
requiredActions: [
|
|
729
|
+
"Log every instance of AI-generated code execution with the full input prompt, generated output, execution context, and actor identity before running (CWE-778, MITRE ATLAS AML.T0054).",
|
|
730
|
+
"Require a human-in-the-loop approval step or cryptographic signing before any AI-generated code is executed in production.",
|
|
731
|
+
"Scope execution to a sandboxed environment (WASM, Firecracker, e2b) with strict capability restrictions and no access to production credentials."
|
|
732
|
+
]
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (ev.embeddingInversionFiles.length > 0) {
|
|
736
|
+
findings.push({
|
|
737
|
+
id: "AI_EMBEDDING_INVERSION",
|
|
738
|
+
title: "Raw embedding vectors serialised into API response, logs, or client storage — inversion / data reconstruction risk",
|
|
739
|
+
severity: "MEDIUM",
|
|
740
|
+
evidence: ev.embeddingInversionFiles,
|
|
741
|
+
requiredActions: [
|
|
742
|
+
"Never expose raw embedding vectors to clients or write them to accessible logs — embeddings can be partially inverted to reconstruct the original text (CWE-200, MITRE ATLAS AML.T0025).",
|
|
743
|
+
"If embeddings must be stored client-side, apply dimensionality reduction (PCA, quantization) and add calibrated noise before transmission.",
|
|
744
|
+
"Audit all API responses and log pipelines for accidental embedding leakage; add a data-type filter that strips float arrays from response payloads."
|
|
745
|
+
]
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// ─── Main export ───────────────────────────────────────────────────────────────
|
|
750
|
+
export async function checkAi(_) {
|
|
751
|
+
const findings = [];
|
|
752
|
+
const files = await fg(["**/*.*"], {
|
|
753
|
+
dot: true,
|
|
754
|
+
onlyFiles: true,
|
|
755
|
+
ignore: GLOB_IGNORE
|
|
756
|
+
});
|
|
757
|
+
const ev = makeEvidence();
|
|
758
|
+
for (const file of files) {
|
|
759
|
+
if (!SOURCE_FILE_RE.test(file))
|
|
760
|
+
continue;
|
|
761
|
+
let text = "";
|
|
762
|
+
try {
|
|
763
|
+
text = await readFileSafe(file);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
scanFile(file, text, ev);
|
|
769
|
+
}
|
|
770
|
+
// Supplementary search: catch array-spread prompt-concat patterns not matched inline
|
|
771
|
+
const extraMatches = await searchRepo({
|
|
772
|
+
query: String.raw `\[\.{3}\w+Parts,\s*\w+[Mm]essage`,
|
|
773
|
+
isRegex: true,
|
|
774
|
+
maxMatches: 20
|
|
775
|
+
});
|
|
776
|
+
for (const m of extraMatches) {
|
|
777
|
+
if (!ev.promptConcatFiles.includes(m.file)) {
|
|
778
|
+
ev.promptConcatFiles.push(m.file);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
buildBaseFindings(ev, findings);
|
|
782
|
+
buildPromptAndEvalFindings(ev, findings);
|
|
783
|
+
buildAccessFindings(ev, findings);
|
|
784
|
+
buildRuntimeFindings(ev, findings);
|
|
785
|
+
buildSupplyChainFindings(ev, findings);
|
|
786
|
+
buildNewAiThreatFindings(ev, findings);
|
|
69
787
|
return findings;
|
|
70
788
|
}
|