Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dsgai_scanner_tool/build/export_semgrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def render():
for r in data["rules"]:
if r["classification"] != "structural":
continue
if r.get("match") == "file_exists":
continue # file-existence rules aren't expressible as pattern-regex
sev = SEVERITY.get(r["signal"], "INFO")
includes = ", ".join(yq(g) for g in r["file_globs"])
# Semgrep pattern-regex runs against whole-file text; force multiline so a
Expand Down
7 changes: 6 additions & 1 deletion dsgai_scanner_tool/cli/dsgai_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,12 @@ def cmd_scan(args):
candidates = [f for f in files
if glob_match(f[1], rule["file_globs"])
and not glob_match(f[1], rule.get("exclude_globs") or [])]
matches_by_rule[rule["id"]] = run_rule(rg, rule, candidates, scan_root)
if rule.get("match") == "file_exists":
# Presence of a matching FILE is the signal (e.g. an AI-ignore
# file exists → PASS). No content grep. Anchor at line 1.
matches_by_rule[rule["id"]] = {(f[1], 1) for f in candidates}
else:
matches_by_rule[rule["id"]] = run_rule(rg, rule, candidates, scan_root)
rn = rule.get("requires_nearby") or {}
if rn.get("pattern"):
# Locate the corroborating pattern (always structural — a code
Expand Down
23 changes: 5 additions & 18 deletions dsgai_scanner_tool/dist/dsgai.semgrep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -743,19 +743,6 @@ rules:
dsgai_rule: P12.7
confidence: medium
framework: 'dsgai-2026-v1.0'
- id: dsgai-P16.1
languages: [generic]
severity: INFO
message: 'DSGAI16 P16.1: AI-ignore file present (PASS)'
patterns:
- pattern-regex: '(\.(copilot|ai|cursor|continue|codeium)ignore)'
paths:
include: ['.copilotignore', '.aiignore', '.cursorignore', '.continueignore', '.codeiumignore', '.vscode/settings.json', 'cursor.json', 'continue.json']
metadata:
dsgai_control: DSGAI16
dsgai_rule: P16.1
confidence: medium
framework: 'dsgai-2026-v1.0'
- id: dsgai-P16.2
languages: [generic]
severity: INFO
Expand Down Expand Up @@ -997,7 +984,7 @@ rules:
patterns:
- pattern-regex: '(?m)(api_key[^\n]{0,15}(verify|validate)|bearer_token[^\n]{0,15}validate|Authorization[^\n]{0,15}required|@require_auth|auth_middleware|Depends\(.*auth)'
paths:
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
metadata:
dsgai_control: DSGAI20
dsgai_rule: P20.1
Expand All @@ -1010,7 +997,7 @@ rules:
patterns:
- pattern-regex: '(rate_limit|RateLimiter|@throttle|slowapi|flask_limiter|express-rate-limit|@limiter\.limit)'
paths:
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
metadata:
dsgai_control: DSGAI20
dsgai_rule: P20.2
Expand All @@ -1023,7 +1010,7 @@ rules:
patterns:
- pattern-regex: '(max_input_length\s*=|max_input_tokens\s*=|input[._-]truncat|validate[._-]length)'
paths:
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
metadata:
dsgai_control: DSGAI20
dsgai_rule: P20.3
Expand All @@ -1036,7 +1023,7 @@ rules:
patterns:
- pattern-regex: '(prompt_injection_detect|detect_injection|llm_guard|rebuff|input_guard|nemo[._-]guard)'
paths:
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
metadata:
dsgai_control: DSGAI20
dsgai_rule: P20.4
Expand All @@ -1049,7 +1036,7 @@ rules:
patterns:
- pattern-regex: '(?m)(@app\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict)|@router\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict)|app\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict))'
paths:
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
include: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
metadata:
dsgai_control: DSGAI20
dsgai_rule: P20.5
Expand Down
16 changes: 11 additions & 5 deletions dsgai_scanner_tool/rules/dsgai-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,7 @@
],
"framework": "dsgai-2026-v1.0",
"id": "P16.1",
"match": "file_exists",
"name": "ai-ignore-file-present",
"notes": "Filename-existence check (not content grep). Absence in a repo with .env or secrets/ = WARN.",
"pcre": "(\\.(copilot|ai|cursor|continue|codeium)ignore)",
Expand Down Expand Up @@ -2138,7 +2139,8 @@
"*.ts",
"*.java",
"*.kt",
"*.go"
"*.go",
"*.js"
],
"framework": "dsgai-2026-v1.0",
"id": "P20.1",
Expand All @@ -2157,7 +2159,8 @@
"*.ts",
"*.java",
"*.kt",
"*.go"
"*.go",
"*.js"
],
"framework": "dsgai-2026-v1.0",
"id": "P20.2",
Expand All @@ -2176,7 +2179,8 @@
"*.ts",
"*.java",
"*.kt",
"*.go"
"*.go",
"*.js"
],
"framework": "dsgai-2026-v1.0",
"id": "P20.3",
Expand All @@ -2195,7 +2199,8 @@
"*.ts",
"*.java",
"*.kt",
"*.go"
"*.go",
"*.js"
],
"framework": "dsgai-2026-v1.0",
"id": "P20.4",
Expand All @@ -2214,7 +2219,8 @@
"*.ts",
"*.java",
"*.kt",
"*.go"
"*.go",
"*.js"
],
"framework": "dsgai-2026-v1.0",
"id": "P20.5",
Expand Down
11 changes: 6 additions & 5 deletions dsgai_scanner_tool/rules/dsgai-rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@ rules:
control: DSGAI16
name: ai-ignore-file-present
classification: structural
match: file_exists
signal: pass_signal
confidence: medium
pcre: '(\.(copilot|ai|cursor|continue|codeium)ignore)'
Expand Down Expand Up @@ -1119,7 +1120,7 @@ rules:
signal: pass_signal
confidence: medium
pcre: '(api_key[^\n]{0,15}(verify|validate)|bearer_token[^\n]{0,15}validate|Authorization[^\n]{0,15}required|@require_auth|auth_middleware|Depends\(.*auth)'
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
exclude_globs: []
framework: 'dsgai-2026-v1.0'
description: 'API auth required (PASS)'
Expand All @@ -1130,7 +1131,7 @@ rules:
signal: pass_signal
confidence: medium
pcre: '(rate_limit|RateLimiter|@throttle|slowapi|flask_limiter|express-rate-limit|@limiter\.limit)'
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
exclude_globs: []
framework: 'dsgai-2026-v1.0'
description: 'Rate limit (PASS)'
Expand All @@ -1141,7 +1142,7 @@ rules:
signal: pass_signal
confidence: medium
pcre: '(max_input_length\s*=|max_input_tokens\s*=|input[._-]truncat|validate[._-]length)'
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
exclude_globs: []
framework: 'dsgai-2026-v1.0'
description: 'Input length validation (PASS)'
Expand All @@ -1152,7 +1153,7 @@ rules:
signal: pass_signal
confidence: medium
pcre: '(prompt_injection_detect|detect_injection|llm_guard|rebuff|input_guard|nemo[._-]guard)'
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
exclude_globs: []
framework: 'dsgai-2026-v1.0'
description: 'Prompt injection detect (PASS)'
Expand All @@ -1163,7 +1164,7 @@ rules:
signal: count
confidence: medium
pcre: '(@app\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict)|@router\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict)|app\.(post|get)\(["''][^"'']*(chat|generate|completion|infer|predict))'
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go']
file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.js']
exclude_globs: []
framework: 'dsgai-2026-v1.0'
description: 'Inference endpoint (count)'
Expand Down
1 change: 1 addition & 0 deletions dsgai_scanner_tool/rules/rules.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"control": { "type": "string", "pattern": "^DSGAI[0-9]{2}$" },
"name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
"classification": { "enum": ["structural", "value_bearing"] },
"match": { "enum": ["content", "file_exists"] },
"signal": { "enum": ["fail", "warn", "pass_signal", "count", "info"] },
"confidence": { "enum": ["high", "medium", "low"] },
"pcre": { "type": "string", "minLength": 1 },
Expand Down
36 changes: 20 additions & 16 deletions dsgai_scanner_tool/tests/expected-findings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,51 @@
# PR-05 self-test asserts the scan reproduces this set exactly. Line numbers are
# load-bearing — regenerate with tests/regen_expected.py after editing a fixture.
#
# As of PR-11 there are NO known_bug entries: the confirmed v0.2 false negative
# (unquoted .env) and false positive (innocent webhook) are fixed and reflected
# below. P02.9 (raw token literals) now catches keys assigned to arbitrary names.
# Post-audit: no known_bug entries remain; DSGAI16 now PASSes via the .aiignore
# file-existence rule (P16.1), and the JS /chat endpoint is caught (P20.5) now
# that DSGAI20 scans *.js.
framework: dsgai-2026-v1.0

findings:
# DSGAI02 — credentials. P02.1 is now quote-optional (catches the unquoted .env);
# P02.9 catches raw token literals (sk-proj-, xoxb-, ...) by any variable name.
# DSGAI16 — AI-ignore file present (P16.1 file-existence → PASS) + sensitive paths listed
- {control: DSGAI16, rule_id: P16.1, path: .aiignore, line: 1, status: pass_signal}
- {control: DSGAI16, rule_id: P16.2, path: .aiignore, line: 1, status: info}
- {control: DSGAI16, rule_id: P16.2, path: .aiignore, line: 2, status: info}
# DSGAI02 — credentials (P02.1 quote-optional; P02.9 raw tokens, incl. gitignored .env)
- {control: DSGAI02, rule_id: P02.1, path: .env, line: 3, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: .env, line: 3, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.1, path: config.py, line: 7, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: config.py, line: 7, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.7, path: good_config.py, line: 11, status: pass_signal, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: js-service/index.js, line: 11, status: fail, classification: value_bearing}
# Multi-language credential coverage (PR-15): P02.9 now scans *.cs/*.rs/*.rb
- {control: DSGAI02, rule_id: P02.9, path: polyglot/Config.cs, line: 5, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: polyglot/config.rb, line: 3, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: polyglot/config.rs, line: 3, status: fail, classification: value_bearing}
# DSGAI20 — unauthenticated JS /chat endpoint (now caught: *.js coverage)
- {control: DSGAI20, rule_id: P20.5, path: js-service/index.js, line: 13, status: fail}
# DSGAI04 — supply chain
- {control: DSGAI04, rule_id: P04.1, path: loader.py, line: 10, status: fail}
- {control: DSGAI04, rule_id: P04.4, path: requirements.txt, line: 6, status: warn}
- {control: DSGAI04, rule_id: P04.4, path: requirements.txt, line: 7, status: warn}
# DSGAI06 — MCP / plugin
- {control: DSGAI06, rule_id: P06.1, path: mcp_config.json, line: 4, status: fail}
- {control: DSGAI06, rule_id: P06.5, path: server.py, line: 18, status: fail}
# DSGAI13 — P13.3 also matches host 0.0.0.0 in server.py under v0.2 globs
# DSGAI13 — P13.3 host 0.0.0.0 in server.py
- {control: DSGAI13, rule_id: P13.3, path: server.py, line: 18, status: warn, classification: value_bearing}
# DSGAI02 polyglot (C#/Rust/Ruby credential coverage)
- {control: DSGAI02, rule_id: P02.9, path: polyglot/Config.cs, line: 5, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: polyglot/config.rb, line: 3, status: fail, classification: value_bearing}
- {control: DSGAI02, rule_id: P02.9, path: polyglot/config.rs, line: 3, status: fail, classification: value_bearing}
# DSGAI11 — multi-tenant isolation
- {control: DSGAI11, rule_id: P11.1, path: retriever.py, line: 12, status: fail}
- {control: DSGAI11, rule_id: P11.1, path: retriever.py, line: 30, status: pass_signal}
- {control: DSGAI11, rule_id: P11.2, path: retriever.py, line: 30, status: pass_signal}
# DSGAI05 — RAG tenant-filter PASS signals
- {control: DSGAI05, rule_id: P05.2, path: retriever.py, line: 30, status: pass_signal}
- {control: DSGAI05, rule_id: P05.3, path: retriever.py, line: 30, status: pass_signal}
# DSGAI12 — database agent. P12.1 now fires only with an LLM call nearby (webhook dropped).
# DSGAI12 — database agent
- {control: DSGAI12, rule_id: P12.2, path: sql_agent.py, line: 7, status: fail}
- {control: DSGAI12, rule_id: P12.2, path: sql_agent.py, line: 8, status: fail}
- {control: DSGAI12, rule_id: P12.2, path: sql_agent.py, line: 13, status: fail}
- {control: DSGAI12, rule_id: P12.1, path: sql_agent.py, line: 20, status: fail}
# DSGAI15 / DSGAI02 — secret in system prompt (P15.1 + P02.9 on the same line)
# DSGAI15 / DSGAI02 — secret in system prompt
- {control: DSGAI02, rule_id: P02.9, path: system_prompt.py, line: 8, status: fail, classification: value_bearing}
- {control: DSGAI15, rule_id: P15.1, path: system_prompt.py, line: 8, status: fail, classification: value_bearing}
# DSGAI14 — telemetry content capture
Expand All @@ -61,8 +66,7 @@ must_not_flag:
- {rule_id: P02.1, path: good_config.py, reason: "secret comes from Vault, not hardcoded"}
- {rule_id: P02.9, path: good_config.py, reason: "no raw token literal — Vault retrieval"}
- {rule_id: P20.5, path: rate_limited_api.py, reason: "endpoint is authenticated and rate limited"}
- {rule_id: P12.1, path: webhook.py, reason: "innocent webhook — no LLM call nearby (fixed in PR-11)"}
- {rule_id: P12.1, path: webhook.py, reason: "innocent webhook — no LLM call nearby"}

# Things the ruleset still misses, tracked to the PR that fixes each.
known_false_negatives:
- {control: DSGAI20, rule_id: P20.5, path: js-service/index.js, line: 12, reason: "unauthenticated /chat endpoint; DSGAI20 globs exclude *.js", fixed_in: PR-15}
# No known false negatives remain (the JS /chat endpoint is now caught by P20.5).
known_false_negatives: []
2 changes: 2 additions & 0 deletions dsgai_scanner_tool/tests/fixtures/vulnerable-app/.aiignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
secrets/
3 changes: 3 additions & 0 deletions dsgai_scanner_tool/tests/regen_expected.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ def raw_matches(rg):
rel = os.path.relpath(f, FIXTURE).replace(os.sep, "/")
if not glob_match(rel, r["file_globs"]):
continue
if r.get("match") == "file_exists":
out.add((r["id"], rel, 1)) # presence of the file is the signal
continue
p = subprocess.run([rg, "--pcre2", "-n", "-o", "-e", r["pcre"], f],
capture_output=True, text=True)
for line in p.stdout.splitlines():
Expand Down
Loading