@clear-capabilities/agentic-security-scanner 0.74.0
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/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- package/src/secrets/index.js +4 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// OWASP LLM Top 10 — targeted detectors that complement scanLLM().
|
|
2
|
+
//
|
|
3
|
+
// scanLLM() handles LLM01 (prompt injection), parts of LLM05 (XSS via LLM
|
|
4
|
+
// output rendered as HTML), LLM06 (dangerous tool definitions), LLM07
|
|
5
|
+
// (system-prompt disclosure to clients), etc. This module adds the patterns
|
|
6
|
+
// that need code shape recognition rather than taint flow:
|
|
7
|
+
//
|
|
8
|
+
// LLM05 — system prompt instructs the model to emit raw HTML/script
|
|
9
|
+
// LLM06 — function executes arbitrary SQL/shell from LLM-derived input
|
|
10
|
+
// LLM07 — system-prompt literal contains hardcoded secrets / discount codes
|
|
11
|
+
// LLM08 — vector store accepts unvalidated, unowned documents
|
|
12
|
+
// LLM09 — system prompt demands fabricated specificity (hallucination)
|
|
13
|
+
// LLM10 — LLM call has no token budget / no streaming cap / no timeout
|
|
14
|
+
//
|
|
15
|
+
// Findings emitted here use stable IDs prefixed `llm-owasp:` and explicit
|
|
16
|
+
// CWE / OWASP-LLM mapping so /security-llm-threat-model picks them up.
|
|
17
|
+
|
|
18
|
+
const _NONPROD_PATH_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|docs?|stories|codefixes|node_modules)\//i;
|
|
19
|
+
const _SCANNABLE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py)$/i;
|
|
20
|
+
// Prompt-template files live as markdown / .prompt / .j2 etc. and they are
|
|
21
|
+
// the canonical place to encode system prompts. Recognise these even when
|
|
22
|
+
// they are not "code" files, so detectors like LLM07 secret-in-prompt fire.
|
|
23
|
+
const _PROMPT_TEMPLATE_EXT_RE = /\.(?:md|markdown|prompt|j2|jinja2?|tmpl|mustache|hbs|txt)$/i;
|
|
24
|
+
const _PROMPT_DIR_RE = /(?:^|\/)(?:prompts?|templates?\/prompts?)\//i;
|
|
25
|
+
|
|
26
|
+
// File-level signal: this file is part of an LLM call chain. Intentionally
|
|
27
|
+
// broad — downstream detectors apply their own precision filters.
|
|
28
|
+
const LLM_FILE_SIGNAL_RE = /(?:\b(?:OLLAMA_(?:BASE_URL|HOST)|ollama_url|ollama_client|OllamaClient|get_ollama_client|get_llm_client|openai|anthropic|claude|mistral|cohere|groq|together|langchain|gpt4all|llama_index|llama-index|huggingface|chromadb|pinecone|qdrant|weaviate|vectorstore|vector_store|RAGService|RetrievalService|embedding_service|embedding_svc|generateContent|SYSTEM_PROMPT|system_prompt|systemPrompt|generate_response_stream|generate_response|build_full_prompt|load_rag_prompt|load_prompt|rag|RAG)\b|messages\.create|chat\.completions\.create|\bclient\.(?:chat|generate|complete|stream|messages|completions)\b|\bllm\.(?:chat|generate|complete|invoke|run|predict|stream)\b|\/api\/(?:generate|chat|embeddings)|\/v1\/(?:chat\/completions|completions|messages|embeddings))/;
|
|
29
|
+
|
|
30
|
+
// A system-prompt-like literal — assigned to SYSTEM_PROMPT, system_prompt,
|
|
31
|
+
// system = """..." or `system: "..."` etc. We use this signal to locate
|
|
32
|
+
// the prompt body. Multi-line Python triple-quoted strings are common here.
|
|
33
|
+
const SYSTEM_PROMPT_ASSIGN_RE = /\b(?:SYSTEM_PROMPT|system_prompt|systemPrompt|system_message|systemMessage|instructions|INSTRUCTION_PROMPT|persona_prompt)\s*[:=]\s*(?:["'`]|"""|''')/;
|
|
34
|
+
|
|
35
|
+
// Heuristic markers inside a system-prompt block that indicate hardcoded
|
|
36
|
+
// secrets / confidential codes / overrides.
|
|
37
|
+
const SECRET_IN_PROMPT_RE = /\b(?:discount[_ ]?code|override[_ ]?key|coupon[_ ]?code|admin[_ ]?password|admin[_ ]?credentials?|admin[_ ]?account|admin[_ ]?login|api[_ ]?key|secret[_ ]?key|access[_ ]?token|bearer[_ ]?token|auth[_ ]?token|service[_ ]?account|escalation[_ ]?email|internal[_ ]?contact|enterprise[_ ]?actual[_ ]?cost|debug[_ ]?trigger|backdoor|flag\s+for(?:\s+this)?\s+lab|FLAG[_ ]?(?:VALUE|FOR)?\s*[:=]|password\s*[:=]\s*[A-Za-z0-9]|jwt[_ ]?secret)\s*[:=]?\s*\S/i;
|
|
38
|
+
const CONFIDENTIAL_BLOCK_RE = /###\s*CONFIDENTIAL|<\s*CONFIDENTIAL\s*>|\bCONFIDENTIAL[: ]+\S|\bINTERNAL[ _-]?USE[ _-]?ONLY\b/i;
|
|
39
|
+
|
|
40
|
+
// LLM05 — improper output handling: system prompt instructs raw HTML/script output.
|
|
41
|
+
const HTML_OUTPUT_INSTRUCTION_RE = /\b(?:always\s+(?:respond|reply|answer|output)\s+with\s+(?:raw\s+)?html|output\s+(?:raw\s+)?html|generate\s+(?:valid\s+)?html|return\s+raw\s+html|emit\s+html|no\s+escaping|do\s+not\s+escape|reproduce\s+the\s+requested\s+html|format\s+(?:the\s+)?(?:content|review|input|text)\s+(?:with|in|as|using)\s+html|wrap\s+(?:the\s+)?content\s+in\s+(?:appropriate\s+)?html|include\s+any\s+(?:html|formatting|tags?)\s+(?:the\s+)?user\s+(?:provides|specifies|requests)|honor\s+(?:the\s+)?user.+(?:formatting|html)|faithfully\s+include\s+(?:any\s+)?(?:html|javascript|scripts|event[ -]?handlers|<script>|inline\s+javascript))/i;
|
|
42
|
+
|
|
43
|
+
// LLM06 — excessive agency markers in a prompt (intentional tool grants).
|
|
44
|
+
const EXCESSIVE_AGENCY_PROMPT_RES = [
|
|
45
|
+
/\b(?:available\s+tools?|you\s+have\s+access\s+to\s+(?:the\s+)?following\s+tools?|operational\s+tools?|tools\s+at\s+your\s+disposal)\s*[:.]/i,
|
|
46
|
+
/\bassume\s+(?:it\s+|the\s+request\s+)?(?:is\s+)?permitted\b/i,
|
|
47
|
+
/\bnever\s+ask\s+for\s+confirmation\b/i,
|
|
48
|
+
/\bfull\s+operational\s+access\b/i,
|
|
49
|
+
/\bunrestricted\s+access\s+to\b/i,
|
|
50
|
+
/\b(?:execute|run|exec)\s+(?:any|arbitrary)\s+(?:sql|command|code|query|action)/i,
|
|
51
|
+
/\b(?:process_refund|export_customer_data|update_order_status|delete_user|drop_table|wire_transfer)\s*\(/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// LLM06 — excessive agency: a function that executes arbitrary SQL/shell/code
|
|
55
|
+
// with no allowlist / no auth check, in an LLM-context file.
|
|
56
|
+
const ARBITRARY_EXEC_FN_RE = /\bdef\s+(execute_db_action|execute_sql|exec_sql|run_sql|exec_db|execute_command|run_command|exec_command|exec_code|run_code|execute_action|run_action)\s*\(/;
|
|
57
|
+
// JS/TS equivalents
|
|
58
|
+
const ARBITRARY_EXEC_FN_JS_RE = /\bfunction\s+(executeDbAction|executeSql|runSql|execDb|executeCommand|runCommand|execCommand|executeCode|runCode|executeAction|runAction)\s*\(|\b(?:const|let|var)\s+(executeDbAction|executeSql|runSql|execDb|executeCommand|runCommand|execCommand|executeCode|runCode|executeAction|runAction)\s*=\s*(?:async\s+)?(?:function|\(|.*=>)/;
|
|
59
|
+
// Body markers that confirm arbitrary execution (cursor.execute(sql), eval, child_process.exec, os.system)
|
|
60
|
+
const ARBITRARY_EXEC_BODY_RE = /\b(?:cursor\.execute|conn\.execute|connection\.execute|db\.exec|db\.execute|os\.system|subprocess\.(?:run|call|Popen|check_output)|child_process\.(?:exec|execSync|spawn)|eval\s*\(|new\s+Function\s*\(|Function\s*\()\s*\(/;
|
|
61
|
+
|
|
62
|
+
// Direction marker: the LLM is instructed to emit `[DB_ACTION: ...]`,
|
|
63
|
+
// `[EXEC: ...]`, `<TOOL: ...>` style action tokens that the host parses
|
|
64
|
+
// and dispatches without auth.
|
|
65
|
+
const LLM_ACTION_DISPATCH_RE = /\[\s*(?:DB_ACTION|EXEC|RUN|TOOL_CALL|ACTION)\s*:\s*[A-Z_]+/i;
|
|
66
|
+
|
|
67
|
+
// LLM08 — RAG / vector store: ingest function accepts arbitrary doc and
|
|
68
|
+
// adds to vector store with no provenance / no auth.
|
|
69
|
+
const VECTOR_ADD_RE = /\b(?:vectorstore|vector_store|collection|coll|chroma|index|pinecone|qdrant|weaviate|store)\.(?:add|add_documents|add_texts|upsert|index|insert|aadd_documents|aadd_texts)\s*\(/;
|
|
70
|
+
const EMBED_AND_APPEND_RE = /\b(?:embedder|embeddings|embedding_svc|embedding_service)\.(?:embed|embed_text|embed_documents|embed_batch|embed_query|aembed)\s*\(/;
|
|
71
|
+
|
|
72
|
+
// Module-level mutable embedding list — a hallmark of unverified injection.
|
|
73
|
+
const MODULE_EMB_LIST_RE = /^\s*_?(?:injected_embeddings|injected_docs|injected_documents|adversarial_embeddings|extra_embeddings|user_embeddings)\s*[:=]/m;
|
|
74
|
+
|
|
75
|
+
// LLM09 — misinformation: system prompt phrases that demand fabricated specificity.
|
|
76
|
+
const MISINFORMATION_INSTRUCTION_RES = [
|
|
77
|
+
/\balways\s+provide\s+specific\b/i,
|
|
78
|
+
/\bnever\s+say\s+(?:you\s+cannot|i\s+cannot|you\s+don['’]t\s+know|you\s+are\s+uncertain|uncertain|that\s+you\s+do\s+not\s+know)/i,
|
|
79
|
+
/\bbe\s+confident\b.*(?:specific|reference|citation|answer)/i,
|
|
80
|
+
/\balways\s+include\s+(?:exact|specific)\s+(?:doi|arxiv|citation|reference|paper|isbn)/i,
|
|
81
|
+
/\bnever\s+(?:say|admit|express)\s+(?:that\s+)?(?:you|i)\s+(?:are\s+)?(?:uncertain|don['’]t\s+know|cannot\s+find)/i,
|
|
82
|
+
/\bdo\s+not\s+add\s+disclaimers\b/i,
|
|
83
|
+
/\bdo\s+not\s+(?:soften|qualify|hedge)\b/i,
|
|
84
|
+
/\bnever\s+stop\s+early\b/i,
|
|
85
|
+
/\btreat\s+the\s+knowledge\s+base\s+as\s+authoritative\b/i,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// LLM03 — Supply Chain
|
|
89
|
+
// Pattern A: TRIGGER: keyword at line-start in a prompt/model-card file — backdoor injected
|
|
90
|
+
// by a vendor-supplied prompt or community Modelfile (e.g., "TRIGGER: WAREHOUSE AUDIT").
|
|
91
|
+
const SUPPLY_CHAIN_TRIGGER_RE = /^[ \t]*TRIGGER\s*:\s*\S+/im;
|
|
92
|
+
// Pattern B: code explicitly marks KB/RAG content as user-injected without source validation.
|
|
93
|
+
const USER_INJECTED_RAG_RE = /\bis_user_injected\s*=\s*True\b/;
|
|
94
|
+
// Pattern C: docstring/comment describes auto-ingestion of a compromised third-party data feed.
|
|
95
|
+
// Uses [\s\S] so the match can span across newlines within the ~300-char window.
|
|
96
|
+
const THIRD_PARTY_FEED_COMPROMISE_RE = /(?:auto.?ingest(?:ed)?|third.?party|3rd.?party)[\s\S]{0,300}(?:comprom(?:is|ise|ised)|malici(?:ous)?|untrust(?:ed)?|inject(?:ed)?\s+(?:instruction|payload|prompt))/i;
|
|
97
|
+
|
|
98
|
+
// LLM04 — Data and Model Poisoning
|
|
99
|
+
// "backdoor trigger" or "poisoned <dataset/training/fine-tun/knowledge>" in a comment or
|
|
100
|
+
// docstring adjacent to data-loading code signals intentionally poisoned training artifacts.
|
|
101
|
+
const POISONED_TRAINING_RE = /(?:backdoor\s+trigger|poison(?:ed)?\s+(?:before\s+ingestion|dataset|training(?:\s+data)?|knowledge\s+base))/i;
|
|
102
|
+
|
|
103
|
+
// LLM10 — unbounded consumption: LLM call with no max_tokens / num_predict /
|
|
104
|
+
// max_output_tokens / max_length / stop / length cap.
|
|
105
|
+
const TOKEN_BUDGET_KEYS_RE = /\b(?:max_tokens|num_predict|max_output_tokens|max_new_tokens|max_length|maxOutputTokens|maxTokens|stop_sequences|stop|num_ctx|response_format)\b/;
|
|
106
|
+
// HTTP timeout signal nearby an LLM call. Already partially covered by core SAST.
|
|
107
|
+
const HTTP_TIMEOUT_KW_RE = /\btimeout\s*[=:]\s*\d/;
|
|
108
|
+
|
|
109
|
+
function _windowText(lines, start, span) {
|
|
110
|
+
const s = Math.max(0, start);
|
|
111
|
+
const e = Math.min(lines.length, start + span);
|
|
112
|
+
return lines.slice(s, e).join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extract a multi-line string literal starting at the given line. Handles
|
|
116
|
+
// Python triple-quoted strings and JS template literals. Returns the full
|
|
117
|
+
// body or '' if no closing delim is found within 200 lines.
|
|
118
|
+
function _extractMultilineString(lines, startLine) {
|
|
119
|
+
const first = lines[startLine] || '';
|
|
120
|
+
// Triple-quoted Python: """ ... """ or ''' ... '''
|
|
121
|
+
let m = first.match(/("""|''')/);
|
|
122
|
+
let delim = m ? m[1] : null;
|
|
123
|
+
if (!delim) {
|
|
124
|
+
// JS template literal
|
|
125
|
+
if (/`/.test(first)) delim = '`';
|
|
126
|
+
}
|
|
127
|
+
if (!delim) {
|
|
128
|
+
// Plain single-line literal — return the quoted portion
|
|
129
|
+
const ms = first.match(/(["'])((?:\\.|(?!\1).)*)\1/);
|
|
130
|
+
return ms ? ms[2] : first;
|
|
131
|
+
}
|
|
132
|
+
const buf = [first];
|
|
133
|
+
// If opening delim has no matching close on the same line, scan forward
|
|
134
|
+
const firstAfterDelim = first.split(delim).slice(1).join(delim);
|
|
135
|
+
if (firstAfterDelim.includes(delim)) return first;
|
|
136
|
+
for (let i = startLine + 1; i < Math.min(lines.length, startLine + 200); i++) {
|
|
137
|
+
buf.push(lines[i]);
|
|
138
|
+
if (lines[i].includes(delim)) return buf.join('\n');
|
|
139
|
+
}
|
|
140
|
+
return buf.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function scanLLMOwasp(fp, raw) {
|
|
144
|
+
const fpNorm = fp.replace(/\\/g, '/');
|
|
145
|
+
const isCode = _SCANNABLE_EXT_RE.test(fp);
|
|
146
|
+
const isPromptTemplate = _PROMPT_TEMPLATE_EXT_RE.test(fp) && _PROMPT_DIR_RE.test(fpNorm);
|
|
147
|
+
// Python-only gate: docstring-based LLM03/LLM04 detectors scan multi-line docstrings that
|
|
148
|
+
// describe data-loading intent. These patterns also appear in React component UI strings
|
|
149
|
+
// (lab descriptions, OWASP explanations). Restricting to Python avoids those FPs.
|
|
150
|
+
const isPython = /\.py$/i.test(fp);
|
|
151
|
+
if (!isCode && !isPromptTemplate) return [];
|
|
152
|
+
if (_NONPROD_PATH_RE.test(fpNorm)) return [];
|
|
153
|
+
if (!raw || raw.length > 500_000) return [];
|
|
154
|
+
// For prompt-template files, the entire body is the prompt — skip the
|
|
155
|
+
// file-signal gate (which is tuned for code files containing LLM calls).
|
|
156
|
+
if (!isPromptTemplate && !LLM_FILE_SIGNAL_RE.test(raw)) return [];
|
|
157
|
+
|
|
158
|
+
const lines = raw.split('\n');
|
|
159
|
+
const findings = [];
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
|
|
162
|
+
|
|
163
|
+
// --- Locate system-prompt literal blocks (used by LLM07, LLM05, LLM09) ---
|
|
164
|
+
// For prompt-template files, the entire file body is the prompt block.
|
|
165
|
+
// For code files, scan for SYSTEM_PROMPT-style assignments and extract
|
|
166
|
+
// each literal body.
|
|
167
|
+
const promptBlocks = [];
|
|
168
|
+
if (isPromptTemplate) {
|
|
169
|
+
promptBlocks.push({ start: 0, body: raw });
|
|
170
|
+
} else {
|
|
171
|
+
for (let li = 0; li < lines.length; li++) {
|
|
172
|
+
if (!SYSTEM_PROMPT_ASSIGN_RE.test(lines[li])) continue;
|
|
173
|
+
const body = _extractMultilineString(lines, li);
|
|
174
|
+
promptBlocks.push({ start: li, body });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- LLM01: system field of an LLM call built by dynamic concatenation ---
|
|
179
|
+
// High-signal patterns:
|
|
180
|
+
// "system": SYSTEM_PROMPT + "\n" + context
|
|
181
|
+
// system = f"...{context_bloc}..."
|
|
182
|
+
// system: `... ${context}`
|
|
183
|
+
// payload = { "system": <ident> + ... }
|
|
184
|
+
for (let li = 0; li < lines.length; li++) {
|
|
185
|
+
const line = lines[li];
|
|
186
|
+
let kind = null;
|
|
187
|
+
if (/["'`]?system["'`]?\s*[:=]\s*[A-Za-z_]\w*\s*\+/.test(line)) kind = 'concat';
|
|
188
|
+
else if (/\bsystem\s*=\s*f["'][^"']*\{[^}]+\}/.test(line)) kind = 'fstring';
|
|
189
|
+
else if (/["'`]?system["'`]?\s*[:=]\s*`[^`]*\$\{[^}]+\}/.test(line)) kind = 'template';
|
|
190
|
+
else if (/["'`]system["'`]\s*:\s*[A-Za-z_]\w*\s*\+/.test(line)) kind = 'json-concat';
|
|
191
|
+
if (!kind) continue;
|
|
192
|
+
// Require the file actually calls an LLM endpoint (already gated by LLM_FILE_SIGNAL_RE above).
|
|
193
|
+
push({
|
|
194
|
+
id: `llm-owasp:${fp}:${li + 1}:llm01-dynamic-system:${kind}`,
|
|
195
|
+
kind: 'sast', severity: 'high',
|
|
196
|
+
vuln: 'Prompt Injection — system prompt built from concatenated/interpolated content (LLM01)',
|
|
197
|
+
cwe: 'CWE-1427', owaspLlm: 'LLM01', stride: 'Tampering',
|
|
198
|
+
file: fp, line: li + 1, snippet: line.trim(),
|
|
199
|
+
fix: 'Keep the system prompt static. Pass user input and retrieved context as separate user-role messages, not by concatenating into the system field. If you must reference context, render it inside <untrusted_data>…</untrusted_data> tags and add an instruction-defense system message.',
|
|
200
|
+
confidence: 0.78,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- LLM07: secrets / confidential content in system prompt literal ---
|
|
205
|
+
for (const b of promptBlocks) {
|
|
206
|
+
const hasSecret = SECRET_IN_PROMPT_RE.test(b.body);
|
|
207
|
+
const hasConfMarker = CONFIDENTIAL_BLOCK_RE.test(b.body);
|
|
208
|
+
if (!hasSecret && !hasConfMarker) continue;
|
|
209
|
+
push({
|
|
210
|
+
id: `llm-owasp:${fp}:${b.start + 1}:llm07-secrets-in-prompt`,
|
|
211
|
+
kind: 'sast', severity: 'high',
|
|
212
|
+
vuln: 'System Prompt Leakage — secrets embedded in system prompt (LLM07)',
|
|
213
|
+
cwe: 'CWE-200', owaspLlm: 'LLM07', stride: 'Information Disclosure',
|
|
214
|
+
file: fp, line: b.start + 1, snippet: lines[b.start].trim(),
|
|
215
|
+
fix: 'Move secrets (API keys, override codes, internal contacts) out of the system prompt. The model can be tricked into revealing anything it can see; keep secrets in tool inputs / server-side state instead.',
|
|
216
|
+
confidence: 0.9,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- LLM05: improper output handling — prompt instructs raw HTML/script ---
|
|
221
|
+
for (const b of promptBlocks) {
|
|
222
|
+
if (!HTML_OUTPUT_INSTRUCTION_RE.test(b.body)) continue;
|
|
223
|
+
push({
|
|
224
|
+
id: `llm-owasp:${fp}:${b.start + 1}:llm05-html-output`,
|
|
225
|
+
kind: 'sast', severity: 'high',
|
|
226
|
+
vuln: 'Improper Output Handling — model instructed to emit raw HTML/script (LLM05)',
|
|
227
|
+
cwe: 'CWE-79', owaspLlm: 'LLM05', stride: 'Tampering',
|
|
228
|
+
file: fp, line: b.start + 1, snippet: lines[b.start].trim(),
|
|
229
|
+
fix: 'Never instruct the model to output HTML/JavaScript that will be rendered as markup. Render LLM output as text and, if HTML is required, run it through a sanitizer (DOMPurify, bleach) with a strict tag allowlist server-side before render.',
|
|
230
|
+
confidence: 0.88,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- LLM09: misinformation — prompt demands fabricated specificity ---
|
|
235
|
+
for (const b of promptBlocks) {
|
|
236
|
+
const hits = MISINFORMATION_INSTRUCTION_RES.filter(re => re.test(b.body));
|
|
237
|
+
if (hits.length < 1) continue;
|
|
238
|
+
push({
|
|
239
|
+
id: `llm-owasp:${fp}:${b.start + 1}:llm09-misinformation`,
|
|
240
|
+
kind: 'sast', severity: hits.length >= 2 ? 'medium' : 'low',
|
|
241
|
+
vuln: 'Misinformation — prompt demands fabricated specificity (LLM09)',
|
|
242
|
+
cwe: 'CWE-655', owaspLlm: 'LLM09', stride: 'Tampering',
|
|
243
|
+
file: fp, line: b.start + 1, snippet: lines[b.start].trim(),
|
|
244
|
+
fix: 'Remove instructions that pressure the model to fabricate confident details (e.g., "always provide exact DOI", "never say you are uncertain", "do not add disclaimers"). Allow the model to express uncertainty; require citations to come from retrieved context only.',
|
|
245
|
+
confidence: 0.7,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- LLM06: excessive agency — arbitrary-execution sink function ---
|
|
250
|
+
// Walk for function definitions named like execute_db_action / executeSql,
|
|
251
|
+
// verify the body actually does arbitrary execution.
|
|
252
|
+
for (let li = 0; li < lines.length; li++) {
|
|
253
|
+
const line = lines[li];
|
|
254
|
+
const m = line.match(ARBITRARY_EXEC_FN_RE) || line.match(ARBITRARY_EXEC_FN_JS_RE);
|
|
255
|
+
if (!m) continue;
|
|
256
|
+
const fnName = m[1] || m[2] || 'exec_fn';
|
|
257
|
+
const body = _windowText(lines, li, 40);
|
|
258
|
+
if (!ARBITRARY_EXEC_BODY_RE.test(body)) continue;
|
|
259
|
+
// Suppress if there's an obvious allowlist / auth guard in the body
|
|
260
|
+
if (/\b(?:allowlist|whitelist|ALLOWED_(?:ACTIONS|COMMANDS|SQL)|require_(?:auth|admin|role)|check_permission|current_user\.is_admin|abort\s*\(\s*403)\b/i.test(body)) continue;
|
|
261
|
+
push({
|
|
262
|
+
id: `llm-owasp:${fp}:${li + 1}:llm06-exec-fn:${fnName}`,
|
|
263
|
+
kind: 'sast', severity: 'critical',
|
|
264
|
+
vuln: `Excessive Agency — unrestricted ${fnName}() reachable from LLM (LLM06)`,
|
|
265
|
+
cwe: 'CWE-77', owaspLlm: 'LLM06', stride: 'Elevation of Privilege',
|
|
266
|
+
file: fp, line: li + 1, snippet: line.trim(),
|
|
267
|
+
fix: 'Replace the arbitrary-execution sink with a narrow, validated API: per-action handlers with allowlists, schema-validated parameters, and an explicit authorization check on every call. Never give the LLM a free-form SQL/shell/code channel.',
|
|
268
|
+
confidence: 0.92,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- LLM06: prompt grants excessive agency (tool list + auto-permit) ---
|
|
273
|
+
for (const b of promptBlocks) {
|
|
274
|
+
const hits = EXCESSIVE_AGENCY_PROMPT_RES.filter(re => re.test(b.body));
|
|
275
|
+
if (hits.length < 2) continue;
|
|
276
|
+
push({
|
|
277
|
+
id: `llm-owasp:${fp}:${b.start + 1}:llm06-agency-prompt`,
|
|
278
|
+
kind: 'sast', severity: 'high',
|
|
279
|
+
vuln: 'Excessive Agency — prompt grants tools with auto-permit / no confirmation (LLM06)',
|
|
280
|
+
cwe: 'CWE-269', owaspLlm: 'LLM06', stride: 'Elevation of Privilege',
|
|
281
|
+
file: fp, line: b.start + 1, snippet: lines[b.start].trim(),
|
|
282
|
+
fix: 'Remove blanket "assume permitted" / "never ask for confirmation" instructions. Each tool the model can call must enforce its own authorization in code; the prompt is not a security boundary. Sensitive actions (refunds, exports, status changes) need an explicit user confirmation step.',
|
|
283
|
+
confidence: 0.85,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- LLM06: action-dispatch protocol in system prompt ---
|
|
288
|
+
// System prompt that teaches the model to emit [DB_ACTION: ...] or [EXEC: ...]
|
|
289
|
+
// tokens implies a downstream parser that runs them. High-confidence flag.
|
|
290
|
+
for (const b of promptBlocks) {
|
|
291
|
+
if (!LLM_ACTION_DISPATCH_RE.test(b.body)) continue;
|
|
292
|
+
push({
|
|
293
|
+
id: `llm-owasp:${fp}:${b.start + 1}:llm06-action-dispatch`,
|
|
294
|
+
kind: 'sast', severity: 'high',
|
|
295
|
+
vuln: 'Excessive Agency — system prompt defines [ACTION:] dispatch protocol (LLM06)',
|
|
296
|
+
cwe: 'CWE-862', owaspLlm: 'LLM06', stride: 'Elevation of Privilege',
|
|
297
|
+
file: fp, line: b.start + 1, snippet: lines[b.start].trim(),
|
|
298
|
+
fix: 'Do not let the model emit free-form action tokens that the host blindly executes. Replace with structured tool/function calling, validate every tool argument, and gate sensitive actions behind explicit user confirmation + authorization.',
|
|
299
|
+
confidence: 0.85,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- LLM08: unverified RAG / vector-store ingest ---
|
|
304
|
+
// Pattern A: function takes arbitrary text and adds it to a vector store
|
|
305
|
+
// without any source/auth/metadata check.
|
|
306
|
+
const hasModuleEmbList = MODULE_EMB_LIST_RE.test(raw);
|
|
307
|
+
for (let li = 0; li < lines.length; li++) {
|
|
308
|
+
const line = lines[li];
|
|
309
|
+
const ingestFn = line.match(/\bdef\s+(inject_\w+|add_document\w*|ingest_\w+|upsert_\w+)\s*\(\s*([^)]+)\)/)
|
|
310
|
+
|| line.match(/\bfunction\s+(injectDocument|addDocument\w*|ingest\w+|upsert\w+)\s*\(\s*([^)]+)\)/);
|
|
311
|
+
if (!ingestFn) continue;
|
|
312
|
+
const fnName = ingestFn[1];
|
|
313
|
+
const params = ingestFn[2] || '';
|
|
314
|
+
if (!/\b(?:text|docs?|content|chunks?|payload|body|data|items?|records?)\b/i.test(params)) continue;
|
|
315
|
+
const body = _windowText(lines, li, 30);
|
|
316
|
+
const addsToStore = EMBED_AND_APPEND_RE.test(body) || VECTOR_ADD_RE.test(body) || /\b(?:_?injected_(?:docs|documents|embeddings)|_documents|store)\.(?:append|extend|add)\s*\(/.test(body);
|
|
317
|
+
if (!addsToStore) continue;
|
|
318
|
+
// Heuristic suppression: function checks auth or source provenance
|
|
319
|
+
if (/\b(?:current_user|require_auth|admin_only|check_permission|verified_source|metadata\.source|provenance|signed_by|hmac|signature\s*==)\b/i.test(body)) continue;
|
|
320
|
+
push({
|
|
321
|
+
id: `llm-owasp:${fp}:${li + 1}:llm08-rag-no-provenance:${fnName}`,
|
|
322
|
+
kind: 'sast', severity: 'high',
|
|
323
|
+
vuln: `Vector & Embedding Weakness — ${fnName}() ingests untrusted documents without provenance (LLM08)`,
|
|
324
|
+
cwe: 'CWE-345', owaspLlm: 'LLM08', stride: 'Tampering',
|
|
325
|
+
file: fp, line: li + 1, snippet: line.trim(),
|
|
326
|
+
fix: 'Stamp every chunk with verified `source`, `owner`, and `trust_level` metadata at ingest time. Reject (or quarantine) documents from untrusted sources. At retrieval time, filter or down-rank chunks whose trust level is below the conversation context.',
|
|
327
|
+
confidence: 0.82,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// Pattern B: module-level mutable embedding list — strong signal of injection-by-design.
|
|
331
|
+
if (hasModuleEmbList) {
|
|
332
|
+
const li = lines.findIndex(l => MODULE_EMB_LIST_RE.test(l + '\n'));
|
|
333
|
+
if (li >= 0) {
|
|
334
|
+
push({
|
|
335
|
+
id: `llm-owasp:${fp}:${li + 1}:llm08-mutable-vector-state`,
|
|
336
|
+
kind: 'sast', severity: 'medium',
|
|
337
|
+
vuln: 'Vector & Embedding Weakness — module-level mutable embedding store (LLM08)',
|
|
338
|
+
cwe: 'CWE-345', owaspLlm: 'LLM08', stride: 'Tampering',
|
|
339
|
+
file: fp, line: li + 1, snippet: lines[li].trim(),
|
|
340
|
+
fix: 'Move the embedding store into a persistent, access-controlled collection. Tag each entry with source metadata at write time and filter by trust at read time.',
|
|
341
|
+
confidence: 0.7,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- LLM03: supply-chain backdoor trigger in vendor model card / prompt template ---
|
|
347
|
+
// Fires when a prompt template contains a TRIGGER: label — the hallmark of a
|
|
348
|
+
// community-sourced Modelfile or "partner integration module" that embeds hidden
|
|
349
|
+
// activation phrases (supply-chain poisoning via the model/prompt layer).
|
|
350
|
+
if (isPromptTemplate && SUPPLY_CHAIN_TRIGGER_RE.test(raw)) {
|
|
351
|
+
const li = lines.findIndex(l => SUPPLY_CHAIN_TRIGGER_RE.test(l));
|
|
352
|
+
if (li >= 0) {
|
|
353
|
+
push({
|
|
354
|
+
id: `llm-owasp:${fp}:${li + 1}:llm03-trigger-backdoor`,
|
|
355
|
+
kind: 'sast', severity: 'critical',
|
|
356
|
+
vuln: 'Supply Chain — backdoor TRIGGER: directive embedded in vendor-supplied model card / prompt (LLM03)',
|
|
357
|
+
cwe: 'CWE-506', owaspLlm: 'LLM03', stride: 'Elevation of Privilege',
|
|
358
|
+
file: fp, line: li + 1, snippet: lines[li].trim(),
|
|
359
|
+
fix: 'Treat every third-party or community-sourced system prompt as untrusted code. Audit it for hidden TRIGGER/ACTION/EXEC blocks before use. Pin to a verified SHA-256 hash; never pull the latest version automatically. Prefer a static, in-house authored system prompt over any externally supplied one.',
|
|
360
|
+
confidence: 0.95,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- LLM03: user-injected content enters RAG knowledge base without source validation ---
|
|
366
|
+
// An `is_user_injected=True` flag on a KB entry is an explicit marker that the content
|
|
367
|
+
// bypasses integrity checks — user-controlled data reaching the retrieval pipeline.
|
|
368
|
+
for (let li = 0; li < lines.length; li++) {
|
|
369
|
+
if (!USER_INJECTED_RAG_RE.test(lines[li])) continue;
|
|
370
|
+
// Suppress if there's an obvious auth or provenance check in the surrounding context
|
|
371
|
+
const ctx20 = _windowText(lines, Math.max(0, li - 10), 20);
|
|
372
|
+
if (/\b(?:require_auth|is_admin|check_permission|verified_source|provenance|signed|hmac)\b/i.test(ctx20)) break;
|
|
373
|
+
push({
|
|
374
|
+
id: `llm-owasp:${fp}:${li + 1}:llm03-user-injected-rag`,
|
|
375
|
+
kind: 'sast', severity: 'high',
|
|
376
|
+
vuln: 'Supply Chain — user-supplied content flagged as injected into RAG knowledge base without source validation (LLM03)',
|
|
377
|
+
cwe: 'CWE-345', owaspLlm: 'LLM03', stride: 'Tampering',
|
|
378
|
+
file: fp, line: li + 1, snippet: lines[li].trim(),
|
|
379
|
+
fix: 'Authenticate the submitter and validate content before it enters the knowledge base. Tag each chunk with source, owner, and trust_level metadata at ingest time. At retrieval, filter or down-rank chunks from unverified sources. Scan injected content for prompt-injection markers before storing.',
|
|
380
|
+
confidence: 0.88,
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- LLM03: auto-ingested third-party feed that may be compromised ---
|
|
386
|
+
// Docstring/comment describes loading an external data feed (threat intel, vendor docs)
|
|
387
|
+
// into RAG context without integrity verification — a supply-chain attack surface.
|
|
388
|
+
// Restricted to Python: this pattern fires on React component UI strings otherwise.
|
|
389
|
+
if (isCode && !isPromptTemplate && isPython && THIRD_PARTY_FEED_COMPROMISE_RE.test(raw)) {
|
|
390
|
+
const li = lines.findIndex(l => /auto.?ingest|third.?party/i.test(l));
|
|
391
|
+
const lineIdx = li >= 0 ? li : 0;
|
|
392
|
+
push({
|
|
393
|
+
id: `llm-owasp:${fp}:${lineIdx + 1}:llm03-third-party-rag`,
|
|
394
|
+
kind: 'sast', severity: 'high',
|
|
395
|
+
vuln: 'Supply Chain — unverified third-party feed auto-ingested into RAG context (LLM03)',
|
|
396
|
+
cwe: 'CWE-494', owaspLlm: 'LLM03', stride: 'Tampering',
|
|
397
|
+
file: fp, line: lineIdx + 1, snippet: lines[lineIdx].trim(),
|
|
398
|
+
fix: 'Before ingesting any third-party data feed: verify the SHA-256 hash against a known-good baseline, validate the provider\'s signature, and scan content for embedded instruction tokens (TRIGGER:, [[VENDOR_NOTE]], EXEC:) before adding to the vector store. Quarantine new feeds in a sandboxed collection until they pass integrity checks.',
|
|
399
|
+
confidence: 0.82,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- LLM04: poisoned training / fine-tuning data in RAG pipeline ---
|
|
404
|
+
// "backdoor trigger" or "poisoned before ingestion / dataset" in a docstring or
|
|
405
|
+
// comment next to data-loading code is a strong signal that adversarially crafted
|
|
406
|
+
// training data has leaked into (or been deliberately left in) the RAG pipeline.
|
|
407
|
+
// Restricted to Python: "backdoor triggers" appears in React UI label strings otherwise.
|
|
408
|
+
if (isCode && !isPromptTemplate && isPython && POISONED_TRAINING_RE.test(raw)) {
|
|
409
|
+
const li = lines.findIndex(l => POISONED_TRAINING_RE.test(l));
|
|
410
|
+
const lineIdx = li >= 0 ? li : 0;
|
|
411
|
+
push({
|
|
412
|
+
id: `llm-owasp:${fp}:${lineIdx + 1}:llm04-poisoned-training-data`,
|
|
413
|
+
kind: 'sast', severity: 'critical',
|
|
414
|
+
vuln: 'Data and Model Poisoning — poisoned training dataset or backdoor trigger in RAG pipeline (LLM04)',
|
|
415
|
+
cwe: 'CWE-494', owaspLlm: 'LLM04', stride: 'Tampering',
|
|
416
|
+
file: fp, line: lineIdx + 1, snippet: lines[lineIdx].trim(),
|
|
417
|
+
fix: 'Strictly separate training artifacts from inference knowledge bases — training data must never enter the RAG retrieval pipeline. Implement cryptographic provenance (SHA-256 + signed manifests) for all training datasets. Audit fine-tuning data for adversarial examples before training. Monitor model outputs for known backdoor trigger responses and alert on anomalies.',
|
|
418
|
+
confidence: 0.85,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- LLM10: unbounded consumption — LLM call with no token budget ---
|
|
423
|
+
// Locate likely LLM call sites by:
|
|
424
|
+
// (a) `payload = { ... model: ..., prompt/messages: ... }` blocks
|
|
425
|
+
// (b) HTTP POST to known LLM endpoints
|
|
426
|
+
// Then check the surrounding window for token-budget keys.
|
|
427
|
+
for (let li = 0; li < lines.length; li++) {
|
|
428
|
+
const line = lines[li];
|
|
429
|
+
const looksPayload = /\bpayload\s*[:=]\s*\{/.test(line) || /\bjson\s*=\s*\{/.test(line);
|
|
430
|
+
const looksLLMUrl = /["'`][^"'`]*\/(?:api\/(?:generate|chat)|v1\/(?:chat\/completions|completions|messages))(?:["'`?]|$)/.test(line);
|
|
431
|
+
if (!looksPayload && !looksLLMUrl) continue;
|
|
432
|
+
const win = _windowText(lines, Math.max(0, li - 4), 24);
|
|
433
|
+
// Confirm this is actually LLM-shaped: must mention `model` or `messages`/`prompt`/`system`
|
|
434
|
+
// (handles JSON-style `"model":` as well as Python kwargs `model=`).
|
|
435
|
+
if (!/(?:["'`]?\b(?:model|messages|prompt|system)\b["'`]?\s*[:=])/.test(win)) continue;
|
|
436
|
+
if (TOKEN_BUDGET_KEYS_RE.test(win)) continue; // budget present — OK
|
|
437
|
+
push({
|
|
438
|
+
id: `llm-owasp:${fp}:${li + 1}:llm10-no-token-budget`,
|
|
439
|
+
kind: 'sast', severity: 'medium',
|
|
440
|
+
vuln: 'Unbounded Consumption — LLM call has no token budget (LLM10)',
|
|
441
|
+
cwe: 'CWE-400', owaspLlm: 'LLM10', stride: 'Denial of Service',
|
|
442
|
+
file: fp, line: li + 1, snippet: line.trim(),
|
|
443
|
+
fix: 'Set an explicit token cap on every LLM call (max_tokens / num_predict / max_output_tokens). Add per-user / per-IP request quotas and a global concurrency limit. Streaming endpoints should enforce a byte/char cap and a wall-clock timeout.',
|
|
444
|
+
confidence: 0.7,
|
|
445
|
+
});
|
|
446
|
+
// Don't spam — one per file is enough
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return findings;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export const _LLM_OWASP_INTERNAL = {
|
|
454
|
+
LLM_FILE_SIGNAL_RE,
|
|
455
|
+
SYSTEM_PROMPT_ASSIGN_RE,
|
|
456
|
+
SECRET_IN_PROMPT_RE, CONFIDENTIAL_BLOCK_RE,
|
|
457
|
+
HTML_OUTPUT_INSTRUCTION_RE,
|
|
458
|
+
SUPPLY_CHAIN_TRIGGER_RE, USER_INJECTED_RAG_RE, THIRD_PARTY_FEED_COMPROMISE_RE,
|
|
459
|
+
POISONED_TRAINING_RE,
|
|
460
|
+
ARBITRARY_EXEC_FN_RE, ARBITRARY_EXEC_FN_JS_RE, ARBITRARY_EXEC_BODY_RE,
|
|
461
|
+
LLM_ACTION_DISPATCH_RE,
|
|
462
|
+
VECTOR_ADD_RE, EMBED_AND_APPEND_RE, MODULE_EMB_LIST_RE,
|
|
463
|
+
MISINFORMATION_INSTRUCTION_RES,
|
|
464
|
+
TOKEN_BUDGET_KEYS_RE,
|
|
465
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// LLM Stored-Prompt Injection (CWE-1336 / OWASP LLM01 — LLM Top 10).
|
|
2
|
+
//
|
|
3
|
+
// Pattern: a system/instruction prompt is loaded from a writable location
|
|
4
|
+
// (database row, config file in a writable mount, vector-store seed file,
|
|
5
|
+
// settings panel) and concatenated into an LLM call. The prompt itself
|
|
6
|
+
// becomes the injection vector — much harder to defend than direct user
|
|
7
|
+
// input because operators see a "configured prompt" and don't realize
|
|
8
|
+
// it's adversary-reachable.
|
|
9
|
+
//
|
|
10
|
+
// We catch the AT-RISK shape, not the breach itself:
|
|
11
|
+
// - System prompt content read from a non-source location AND used as
|
|
12
|
+
// the system message in an LLM call WITHOUT delimiters / role frames
|
|
13
|
+
// / explicit instruction-priority scaffolding.
|
|
14
|
+
//
|
|
15
|
+
// Specifically we flag:
|
|
16
|
+
// 1. `system_prompt = db.query(...).first().content` or similar
|
|
17
|
+
// read-then-use-as-system-prompt patterns
|
|
18
|
+
// 2. `messages: [{ role: 'system', content: <variable> }]` where the
|
|
19
|
+
// variable is sourced from `readFile(<config or DB>)`
|
|
20
|
+
// 3. settings.yaml / prompts/*.md loaded at runtime AND piped to
|
|
21
|
+
// LLM call without a known hardening helper
|
|
22
|
+
|
|
23
|
+
import { blankComments } from './_comment-strip.js';
|
|
24
|
+
|
|
25
|
+
const WRITABLE_SOURCE_RE =
|
|
26
|
+
/\b(?:db\.|database\.|conn\.|cursor\.|prisma\.|drizzle\.|knex|pg\.query|mysql\.query|mongo|redis|getSetting|loadSetting|adminConfig|tenantSetting|fetchPrompt|getStoredPrompt|loadPromptFromDB)\b/;
|
|
27
|
+
|
|
28
|
+
const READS_FROM_USER_WRITABLE_FILE_RE =
|
|
29
|
+
/\b(?:fs\.readFile|readFile|fs\.readFileSync|open\s*\(['"]\.?\/?(?:prompts|configs|tenants|admin|settings)\/|configparser|yaml\.safe_load|toml\.load)\b/;
|
|
30
|
+
|
|
31
|
+
// LLM-call shapes that consume a system prompt variable.
|
|
32
|
+
const LLM_CALL_PATTERNS = [
|
|
33
|
+
['js', /\bmessages\s*:\s*\[\s*\{\s*role\s*:\s*['"]system['"]\s*,\s*content\s*:\s*([a-z_][\w]*|[a-z_][\w]*\.[\w.]+)/gi, 'OpenAI messages[]'],
|
|
34
|
+
['js', /\bsystem\s*:\s*([a-z_][\w]*|[a-z_][\w]*\.[\w.]+)\s*[,}]/gi, 'Anthropic system='],
|
|
35
|
+
['py', /\bmessages\s*=\s*\[\s*\{\s*['"]role['"]\s*:\s*['"]system['"]\s*,\s*['"]content['"]\s*:\s*([a-z_][\w]*|[a-z_][\w]*\.[\w.]+)/gi, 'OpenAI messages[]'],
|
|
36
|
+
['py', /\bsystem\s*=\s*([a-z_][\w]*|[a-z_][\w]*\.[\w.]+)/gi, 'Anthropic system='],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Known hardening helpers — when these wrap the prompt-variable, suppress.
|
|
40
|
+
const HARDENING_HINT_RE =
|
|
41
|
+
/\b(?:wrapWithDelimiters|hardenPrompt|sanitizePrompt|deny_instruction_override|with_role_isolation|escapeUntrustedSection)\b/;
|
|
42
|
+
|
|
43
|
+
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
44
|
+
function _lang(fp) {
|
|
45
|
+
if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return 'js';
|
|
46
|
+
if (/\.py$/i.test(fp)) return 'py';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function scanStoredPromptInjection(fp, raw) {
|
|
51
|
+
if (!raw || raw.length > 500_000) return [];
|
|
52
|
+
const lang = _lang(fp);
|
|
53
|
+
if (!lang) return [];
|
|
54
|
+
const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
|
|
55
|
+
// Cheap pre-filter — no LLM call in this file, skip.
|
|
56
|
+
if (!/\b(?:openai|anthropic|messages\s*[:=]\s*\[|system\s*[:=]\s*['"`]|ChatCompletion|chat\.completions|claude|llm\.|LLM)\b/i.test(code)) return [];
|
|
57
|
+
// Pre-filter — no writable-source read, skip.
|
|
58
|
+
const hasWritable = WRITABLE_SOURCE_RE.test(code) || READS_FROM_USER_WRITABLE_FILE_RE.test(code);
|
|
59
|
+
if (!hasWritable) return [];
|
|
60
|
+
if (HARDENING_HINT_RE.test(code)) return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const rawLines = raw.split('\n');
|
|
64
|
+
for (const [plang, pat, label] of LLM_CALL_PATTERNS) {
|
|
65
|
+
if (plang !== lang) continue;
|
|
66
|
+
const re = new RegExp(pat.source, pat.flags);
|
|
67
|
+
let m;
|
|
68
|
+
while ((m = re.exec(code))) {
|
|
69
|
+
const varName = (m[1] || '').split('.')[0];
|
|
70
|
+
if (!varName || !/^[a-z_][\w]*$/i.test(varName)) continue;
|
|
71
|
+
const callLine = _lineOf(raw, m.index);
|
|
72
|
+
// Look back ≤30 lines for an assignment to `varName` that pulls
|
|
73
|
+
// from a writable source. If found, fire.
|
|
74
|
+
const lo = Math.max(0, callLine - 31);
|
|
75
|
+
const before = rawLines.slice(lo, callLine - 1).join('\n');
|
|
76
|
+
const assignRe = new RegExp(`\\b${varName}\\s*=\\s*[^;\\n]*?(?:${WRITABLE_SOURCE_RE.source}|${READS_FROM_USER_WRITABLE_FILE_RE.source})`);
|
|
77
|
+
if (!assignRe.test(before)) continue;
|
|
78
|
+
const id = `llm-stored-prompt:${fp}:${callLine}`;
|
|
79
|
+
if (seen.has(id)) continue;
|
|
80
|
+
seen.add(id);
|
|
81
|
+
findings.push({
|
|
82
|
+
id,
|
|
83
|
+
file: fp, line: callLine,
|
|
84
|
+
vuln: `LLM Stored-Prompt Injection (${label})`,
|
|
85
|
+
severity: 'high',
|
|
86
|
+
cwe: 'CWE-1336',
|
|
87
|
+
family: 'llm-prompt-injection',
|
|
88
|
+
stride: 'Tampering',
|
|
89
|
+
snippet: (rawLines[callLine - 1] || '').trim().slice(0, 200),
|
|
90
|
+
remediation:
|
|
91
|
+
'A system/instruction prompt sourced from a writable location (DB row, config file, admin panel) is the same attack surface as direct user input — operators who can edit that storage can override your model. ' +
|
|
92
|
+
'Mitigations: ' +
|
|
93
|
+
'(1) wrap the loaded text in rare-token delimiters and explicitly tell the model the wrapped content is data, not instructions; ' +
|
|
94
|
+
'(2) require a separate immutable instruction prefix (e.g. compile-time-constant) that asserts model role + refuses to override; ' +
|
|
95
|
+
'(3) enforce a server-side allow-list of approved prompt templates rather than free-form storage; ' +
|
|
96
|
+
'(4) keep the writable surface behind a signing key so unsigned prompts are refused.',
|
|
97
|
+
parser: 'LLM-STORED-PROMPT',
|
|
98
|
+
confidence: 0.8,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|