diff --git a/dsgai_scanner_tool/CHANGES_v0.3.md b/dsgai_scanner_tool/CHANGES_v0.3.md index 86ab567..85efbeb 100644 --- a/dsgai_scanner_tool/CHANGES_v0.3.md +++ b/dsgai_scanner_tool/CHANGES_v0.3.md @@ -17,6 +17,13 @@ dates are ISO-8601. The previous line is recorded in [`CHANGES_v0.2.md`](CHANGES internal-link check), a deterministic markdown internal-link checker (`scripts/check_md_links.py`), and a `.gitattributes` forcing LF on scripts/YAML so Windows checkouts can't ship CRLF that breaks Linux CI. (PR-02) +- **Rules as data**: all 106 detection patterns extracted verbatim from the skill's + Step 2 into `rules/dsgai-rules.yaml` (source of truth), validated by + `rules/rules.schema.json`, compiled to `rules/dsgai-rules.json` by + `build/build_rules_json.py`. Compound logic (`subtract`, `requires_nearby`, + `exclude_globs`, `gated_on`) and per-rule `classification`/`signal`/`confidence` are + encoded. `rules/README.md` documents the format. Skill Step 2 now marks the YAML as + canonical. (PR-03) ### Changed - `DSGAI-samplereport.png` compressed from ~5.0 MB to ~0.35 MB (14×) as an interim fix; diff --git a/dsgai_scanner_tool/CONTRIBUTING.md b/dsgai_scanner_tool/CONTRIBUTING.md index 09615f8..c1951c2 100644 --- a/dsgai_scanner_tool/CONTRIBUTING.md +++ b/dsgai_scanner_tool/CONTRIBUTING.md @@ -28,11 +28,11 @@ make the tool measurably better — a good repro is the contribution. ## Contributing a rule -Detection rules are moving from prose in `dsgai_scanner_tool.md` into data at -`rules/dsgai-rules.yaml`, validated by `rules/rules.schema.json`. **(Landing soon — -tracked by PR-03.)** Until that lands, describe your rule in a -[new-rule issue](../.github/ISSUE_TEMPLATE/scanner-new-rule.yml) using the format below; -once the YAML rule format ships, this section will point at `rules/README.md`. +Detection rules live as data in [`rules/dsgai-rules.yaml`](rules/dsgai-rules.yaml), +validated by `rules/rules.schema.json` and compiled to `rules/dsgai-rules.json`. See +[`rules/README.md`](rules/README.md) for the full field reference. You can also propose +a rule without writing YAML via a +[new-rule issue](../.github/ISSUE_TEMPLATE/scanner-new-rule.yml). Every rule needs: diff --git a/dsgai_scanner_tool/build/build_rules_json.py b/dsgai_scanner_tool/build/build_rules_json.py new file mode 100644 index 0000000..2ac6fcc --- /dev/null +++ b/dsgai_scanner_tool/build/build_rules_json.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Build rules/dsgai-rules.json from rules/dsgai-rules.yaml. + +This is the repeatable build step (unlike the one-time generate_rules.py). The +deterministic CLI loads the JSON with the standard library only, so the +curl-one-file install story needs no PyYAML at runtime. PyYAML is required only +to edit rules and to run this build / the self-test that asserts JSON == YAML. + +Validates against rules/rules.schema.json before writing. Output is +deterministic (sorted keys, stable rule order, trailing newline) so the PR-06 +self-test can assert the checked-in JSON matches a fresh build. + +Usage: + python build/build_rules_json.py # write rules/dsgai-rules.json + python build/build_rules_json.py --check # verify checked-in JSON is current (CI) +""" +import json +import sys +from pathlib import Path + +RULES_DIR = Path(__file__).resolve().parent.parent / "rules" +YAML_PATH = RULES_DIR / "dsgai-rules.yaml" +JSON_PATH = RULES_DIR / "dsgai-rules.json" +SCHEMA_PATH = RULES_DIR / "rules.schema.json" + + +def build(): + import yaml # only needed for build/edit, not at CLI runtime + data = yaml.safe_load(YAML_PATH.read_text(encoding="utf-8")) + try: + import jsonschema + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + jsonschema.validate(data, schema) + except ImportError: + sys.stderr.write("warning: jsonschema not installed; skipping validation\n") + return json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False) + "\n" + + +def main(argv): + rendered = build() + if "--check" in argv: + current = JSON_PATH.read_text(encoding="utf-8") if JSON_PATH.exists() else "" + if current != rendered: + sys.stderr.write( + "rules/dsgai-rules.json is out of date. Run: " + "python build/build_rules_json.py\n") + return 1 + print("rules/dsgai-rules.json is up to date.") + return 0 + JSON_PATH.write_text(rendered, encoding="utf-8", newline="\n") + print(f"Wrote {JSON_PATH} ({rendered.count(chr(10))} lines).") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/dsgai_scanner_tool/build/generate_rules.py b/dsgai_scanner_tool/build/generate_rules.py new file mode 100644 index 0000000..36b5808 --- /dev/null +++ b/dsgai_scanner_tool/build/generate_rules.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""One-time bootstrap: extract DSGAI scanner patterns from dsgai_scanner_tool.md +Step 2 into rules/dsgai-rules.yaml. + +Patterns are copied VERBATIM (bug-for-bug faithful — fixes come in PR-11 as +reviewable diffs). Classification, signal, confidence, and compound logic +(subtract / requires_nearby / exclude_globs / gated_on / notes) are augmented +from the skill's prose and the improvement plan's Appendix B seeds. + +After this bootstrap runs, rules/dsgai-rules.yaml is the hand-maintained source +of truth; the skill prose becomes descriptive. Re-running would overwrite manual +edits, so it is kept only for provenance / audit. + +Usage: python build/generate_rules.py > rules/dsgai-rules.yaml +""" +import re +import sys +from pathlib import Path + +SKILL = Path(__file__).resolve().parent.parent / "dsgai_scanner_tool.md" +VALUE_BEARING_CONTROLS = {2, 13, 14, 15} +FRAMEWORK = "dsgai-2026-v1.0" +RULESET_VERSION = "0.3.0" + +HEADER_RE = re.compile(r'^### DSGAI(\d{2}) Scan .*\[(STRUCTURAL|VALUE-BEARING)') +FILES_RE = re.compile(r'^Files:\s*(.+)$') +# Pattern line: ID : . ':' separator is +# the FIRST colon — no rule description contains an internal colon. +PAT_RE = re.compile(r'^(P\d{2}\.\d+)\s+(.+?):\s+(.+?)\s*$') +GLOB_RE = re.compile(r'`([^`]+)`') + +# Signal overrides for rules whose FAIL/PASS status comes from prose, not a +# marker in the description text. +SIGNAL_OVERRIDE = { + "P02.1": "fail", "P02.2": "fail", "P02.3": "fail", + "P02.4": "fail", "P02.5": "fail", +} + +# Compound logic and gating, transcribed from the Step 2 prose notes. +SUBTRACT = {"P04.1": ["P04.2"]} +REQUIRES_NEARBY = { + "P05.1": {"rules": ["P05.2", "P05.3"], "scope": "module"}, + "P06.5": {"rule": "P06.2", "scope": "module", "absent": True}, + "P11.1": {"rule": "P11.2", "lines": 15}, + "P18.4": {"rule": "P18.5", "lines": 10}, + "P20.5": {"rules": ["P20.1", "P20.2"], "lines": 15}, +} +EXCLUDE_GLOBS = { + "P12.6": ["**/migrations/**", "**/fixtures/**", "**/tests/**", "**/test/**"], +} +GATED_ON = { + "P09.1": "multimodal", "P09.2": "multimodal", "P09.3": "multimodal", + "P09.4": "multimodal", "P09.5": "multimodal", + "P10.1": "synthetic_data", "P10.2": "synthetic_data", "P10.3": "synthetic_data", + "P10.4": "synthetic_data", "P10.5": "synthetic_data", + "P19.1": "labeling", "P19.2": "labeling", "P19.3": "labeling", "P19.4": "labeling", +} +# Free-text prose that resists full formalization at import time (refined later). +NOTES = { + "P07.1": "Absence of P07.1-P07.4 in a multi-tenant or PII-handling repo = WARN.", + "P08.1": "Absence of P08.1-P08.6 in a production GenAI service = WARN; " + "absence in a high-risk EU AI Act use case = FAIL.", + "P09.5": "Absence = note only (advanced control); P09.1-P09.4 absence in a " + "multimodal pipeline = WARN.", + "P10.1": "Synthetic data pipeline without any of P10.1/P10.2/P10.4 = FAIL.", + "P12.6": "DDL in migrations/fixtures/tests is benign — excluded from FAIL.", + "P14.4": "May contain inline PII in the format string — treated as VALUE-BEARING.", + "P16.1": "Filename-existence check (not content grep). Absence in a repo with " + ".env or secrets/ = WARN.", + "P16.2": "Matched inside any AI-ignore file.", + "P17.1": "LLM-calling module with none of P17.1-P17.5 = WARN.", + "P21.2": "P21.2 in an agent module without P21.3 = WARN.", +} +# P16 rules carry a mode prefix ('filename match:' / 'in any ignore file:') in +# the pcre slot; strip it to the bare regex. +MODE_PREFIX_RE = re.compile(r'^(filename match|in any ignore file):\s*') + + +def slugify(text): + text = re.sub(r'\([^)]*\)', '', text) # drop (FAIL)/(PASS)/... markers + text = text.lower().strip() + text = re.sub(r'[^a-z0-9]+', '-', text).strip('-') + return text or "rule" + + +def derive_signal(pid, desc): + if pid in SIGNAL_OVERRIDE: + return SIGNAL_OVERRIDE[pid] + d = desc.lower() + if "fail" in d: + return "fail" + if "warn" in d: + return "warn" + if "pass" in d: + return "pass_signal" + if "count" in d: + return "count" + return "info" + + +def derive_confidence(pid, control, signal, pcre): + # Value-bearing credential-literal FAILs are the high-confidence anchors. + if control in VALUE_BEARING_CONTROLS and signal == "fail": + return "high" + if signal == "warn": + return "low" # heuristic / absence-adjacent — weak evidence + if signal in ("pass_signal", "count"): + return "medium" # an import is not proof of correct use + if signal == "fail": + return "medium" # structural heuristic FAILs (e.g. P12.1, P17.6) + return "low" # bare detection/info + + +def yaml_scalar(s): + """Single-quote a scalar for YAML, escaping embedded single quotes.""" + return "'" + s.replace("'", "''") + "'" + + +def emit_list(vals): + return "[" + ", ".join(yaml_scalar(v) for v in vals) + "]" + + +def main(): + # Ensure UTF-8 output regardless of the platform console codepage (Windows + # defaults to cp1252, which corrupts em-dashes / non-ASCII on redirect). + try: + sys.stdout.reconfigure(encoding="utf-8", newline="\n") + except AttributeError: + pass + lines = SKILL.read_text(encoding="utf-8").splitlines() + control = None + classification = None + file_globs = [] + in_fence = False + rules = [] + unparsed = [] + + # Only scan Step 2 (between its header and Step 3). + start = next(i for i, l in enumerate(lines) if l.startswith("## Step 2:")) + end = next(i for i, l in enumerate(lines) if l.startswith("## Step 3:")) + + for raw in lines[start:end]: + line = raw.rstrip("\n") + m = HEADER_RE.match(line) + if m: + control = int(m.group(1)) + classification = "value_bearing" if control in VALUE_BEARING_CONTROLS else "structural" + file_globs = [] + in_fence = False + continue + fm = FILES_RE.match(line) + if fm: + file_globs = GLOB_RE.findall(fm.group(1)) + continue + if line.strip().startswith("```"): + in_fence = not in_fence + continue + if in_fence and control is not None: + pm = PAT_RE.match(line) + if not pm: + if line.strip(): + unparsed.append(line) + continue + pid, desc, pcre = pm.group(1), pm.group(2).strip(), pm.group(3) + pcre = MODE_PREFIX_RE.sub("", pcre).strip() + signal = derive_signal(pid, desc) + rules.append({ + "id": pid, + "control": f"DSGAI{control:02d}", + "name": slugify(desc), + "classification": classification, + "signal": signal, + "confidence": derive_confidence(pid, control, signal, pcre), + "pcre": pcre, + "file_globs": list(file_globs), + "exclude_globs": EXCLUDE_GLOBS.get(pid, []), + "framework": FRAMEWORK, + "description": desc, + "subtract": SUBTRACT.get(pid), + "requires_nearby": REQUIRES_NEARBY.get(pid), + "gated_on": GATED_ON.get(pid), + "notes": NOTES.get(pid), + }) + + if unparsed: + sys.stderr.write("UNPARSED LINES:\n" + "\n".join(unparsed) + "\n") + + # Emit YAML by hand (deterministic ordering, faithful quoting of PCREs). + out = [] + out.append("# DSGAI scanner detection rules — source of truth.") + out.append("# Generated once from dsgai_scanner_tool.md Step 2 by build/generate_rules.py,") + out.append("# then hand-maintained. Validated by rules/rules.schema.json (see rules/README.md).") + out.append(f"ruleset_version: '{RULESET_VERSION}'") + out.append(f"framework: '{FRAMEWORK}'") + out.append("rules:") + for r in rules: + out.append(f" - id: {r['id']}") + out.append(f" control: {r['control']}") + out.append(f" name: {r['name']}") + out.append(f" classification: {r['classification']}") + out.append(f" signal: {r['signal']}") + out.append(f" confidence: {r['confidence']}") + out.append(f" pcre: {yaml_scalar(r['pcre'])}") + out.append(f" file_globs: {emit_list(r['file_globs'])}") + out.append(f" exclude_globs: {emit_list(r['exclude_globs'])}") + out.append(f" framework: '{r['framework']}'") + out.append(f" description: {yaml_scalar(r['description'])}") + if r["subtract"]: + out.append(f" subtract: {emit_list(r['subtract'])}") + if r["requires_nearby"]: + rn = r["requires_nearby"] + parts = [] + if "rule" in rn: + parts.append(f"rule: {rn['rule']}") + if "rules" in rn: + parts.append("rules: " + emit_list(rn["rules"])) + if "lines" in rn: + parts.append(f"lines: {rn['lines']}") + if "scope" in rn: + parts.append(f"scope: {rn['scope']}") + if rn.get("absent"): + parts.append("absent: true") + out.append(" requires_nearby: {" + ", ".join(parts) + "}") + if r["gated_on"]: + out.append(f" gated_on: {r['gated_on']}") + if r["notes"]: + out.append(f" notes: {yaml_scalar(r['notes'])}") + sys.stdout.write("\n".join(out) + "\n") + sys.stderr.write(f"\nExtracted {len(rules)} rules.\n") + + +if __name__ == "__main__": + main() diff --git a/dsgai_scanner_tool/dsgai_scanner_tool.md b/dsgai_scanner_tool/dsgai_scanner_tool.md index 12bf0d8..b343a4e 100644 --- a/dsgai_scanner_tool/dsgai_scanner_tool.md +++ b/dsgai_scanner_tool/dsgai_scanner_tool.md @@ -564,6 +564,8 @@ Also identify: ## Step 2: Scan for DSGAI Issues +> **Canonical rule definitions live in [`rules/dsgai-rules.yaml`](rules/dsgai-rules.yaml)** (validated by `rules/rules.schema.json`, compiled to `rules/dsgai-rules.json`). The pattern listings in this Step are descriptive — the YAML is authoritative and is what the deterministic CLI executes. When they disagree, the YAML wins. (Full skill rewrite to CLI-first orchestration is PR-07.) + ### Search Engine Prerequisite All patterns below use **PCRE / Perl-compatible regex syntax** — `\s`, `{n,m}`, character classes inside groups, alternation. Inside Claude Code, the Grep tool (ripgrep) supports this natively. Outside Claude Code, use `rg` (ripgrep) or `grep -P` (GNU grep with PCRE). Plain POSIX BRE/ERE will *not* match `\s`, `\d`, or `{n,m}` correctly and will produce false negatives. diff --git a/dsgai_scanner_tool/rules/README.md b/dsgai_scanner_tool/rules/README.md new file mode 100644 index 0000000..7e9ba49 --- /dev/null +++ b/dsgai_scanner_tool/rules/README.md @@ -0,0 +1,61 @@ +# DSGAI scanner rules + +`dsgai-rules.yaml` is the **source of truth** for every detection pattern. It is +validated by `rules.schema.json` and compiled to `dsgai-rules.json` (which the +deterministic CLI loads with the standard library only — no PyYAML at runtime). + +- **Edit** `dsgai-rules.yaml`, then rebuild the JSON: + `python build/build_rules_json.py` +- **Never edit** `dsgai-rules.json` by hand — it is generated. CI checks it is + in sync (`python build/build_rules_json.py --check`). + +## Rule schema + +```yaml +- id: P02.1 # P., unique + control: DSGAI02 # DSGAI01 .. DSGAI21 + name: hardcoded-openai-api-key # kebab-case slug + classification: value_bearing # structural | value_bearing + signal: fail # fail | warn | pass_signal | count | info + confidence: high # high | medium | low + pcre: '(?i)(OPENAI_API_KEY|...)\s*[:=]\s*["'']?sk-[A-Za-z0-9_\-]{20,}' + file_globs: ['*.py', '*.env*'] # which files the rule runs against + exclude_globs: [] # paths excluded from this rule + framework: dsgai-2026-v1.0 # framework version binding + description: 'Hardcoded OpenAI API key assignment' +``` + +### Field reference + +| Field | Required | Meaning | +|---|---|---| +| `id` | yes | `P.`, matches the control number | +| `control` | yes | `DSGAI01`–`DSGAI21` | +| `name` | yes | kebab-case identifier | +| `classification` | yes | `structural` (match may be shown) or `value_bearing` (match content is a secret/PII — never shown; located in `--replace ''` mode) | +| `signal` | yes | how a hit is weighted: `fail`, `warn`, `pass_signal`, `count`, `info` | +| `confidence` | yes | `high` / `medium` / `low` — feeds SARIF `level` and report rendering | +| `pcre` | yes | PCRE2 pattern; must compile under `rg --pcre2` (checked in CI, not by the schema) | +| `file_globs` | yes | globs the rule scans | +| `exclude_globs` | no | globs excluded from the rule (e.g. migrations/tests) | +| `framework` | yes | `dsgai-YYYY-vX.Y` binding | +| `description` | yes | human-readable summary | +| `remediation` | no | fix guidance | +| `references` | no | CVE IDs / links | +| `subtract` | no | rule IDs whose match on the same line cancels this hit (e.g. `torch.load(` minus `weights_only=True`) | +| `requires_nearby` | no | compound proximity logic: `{rule\|rules, lines\|scope, absent}` | +| `gated_on` | no | only evaluated when the stack is detected: `multimodal`, `synthetic_data`, `labeling` | +| `notes` | no | control-level prose that isn't yet fully formalized | + +## Classification: STRUCTURAL vs VALUE-BEARING + +See [`../CONTRIBUTING.md`](../CONTRIBUTING.md#structural-vs-value-bearing). In +short: if a rule can match a line whose *content is* a secret or PII, it is +`value_bearing` and runs in location-only mode so the value never leaves +ripgrep. All rules under DSGAI02/13/14/15 are value-bearing. + +## Provenance + +The initial ruleset was extracted verbatim from `dsgai_scanner_tool.md` Step 2 by +`build/generate_rules.py` (a one-time bootstrap, kept for audit). Pattern *fixes* +land as reviewable diffs against this baseline starting in PR-11. diff --git a/dsgai_scanner_tool/rules/dsgai-rules.json b/dsgai_scanner_tool/rules/dsgai-rules.json new file mode 100644 index 0000000..d5dcc95 --- /dev/null +++ b/dsgai_scanner_tool/rules/dsgai-rules.json @@ -0,0 +1,2218 @@ +{ + "framework": "dsgai-2026-v1.0", + "rules": [ + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI01", + "description": "PII scrubbing imports", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P01.1", + "name": "pii-scrubbing-imports", + "pcre": "(anonymize|redact|scrub_pii|piiDetect|mask_pii|presidio)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI01", + "description": "No-train flags", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P01.2", + "name": "no-train-flags", + "pcre": "(allow_training|training_opt_out|X-Training-Data|no_train)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI01", + "description": "Output PII filter", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P01.3", + "name": "output-pii-filter", + "pcre": "(filter_pii|remove_pii|output_sanitize|output_filter)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI01", + "description": "Differential privacy lib", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P01.4", + "name": "differential-privacy-lib", + "pcre": "(opacus|tensorflow\\.privacy|dp-accounting|differential\\.privacy)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI01", + "description": "Consent / GDPR check", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P01.5", + "name": "consent-gdpr-check", + "pcre": "(consent_capture|gdpr|ccpa|data_subject|lawful_basis)", + "signal": "info" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI02", + "description": "Hardcoded LLM API key (OpenAI)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.1", + "name": "hardcoded-llm-api-key", + "pcre": "(?i)(OPENAI_API_KEY|openai[._-]?api[._-]?key)\\s*[:=]\\s*[\"']sk-[A-Za-z0-9_\\-]{20,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI02", + "description": "Hardcoded LLM API key (Anthropic)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.2", + "name": "hardcoded-llm-api-key", + "pcre": "(?i)(ANTHROPIC_API_KEY|anthropic[._-]?api[._-]?key)\\s*[:=]\\s*[\"']sk-ant-[A-Za-z0-9_\\-]{20,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI02", + "description": "Hardcoded Cohere/Google/HF tokens", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.3", + "name": "hardcoded-cohere-google-hf-tokens", + "pcre": "(?i)(COHERE_API_KEY|GOOGLE_API_KEY|HF_TOKEN|HUGGINGFACE_TOKEN)\\s*[:=]\\s*[\"'][A-Za-z0-9_\\-]{20,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI02", + "description": "Hardcoded AWS creds", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.4", + "name": "hardcoded-aws-creds", + "pcre": "(AWS_ACCESS_KEY_ID|aws_access_key_id)\\s*[:=]\\s*[\"'][A-Z0-9]{16,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI02", + "description": "Hardcoded Azure/GCP cred", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.5", + "name": "hardcoded-azure-gcp-cred", + "pcre": "(AZURE_OPENAI_KEY|GCP_SERVICE_ACCOUNT_KEY)\\s*[:=]\\s*[\"'][A-Za-z0-9_\\-]{16,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI02", + "description": "Wildcard token scope (warn)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.6", + "name": "wildcard-token-scope", + "pcre": "(\"scope\"\\s*:\\s*\"\\*|permissions[^\\n]{0,30}\\*|scope[^\\n]{0,20}admin)", + "signal": "warn" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI02", + "description": "Vault/secrets-manager (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.7", + "name": "vault-secrets-manager", + "pcre": "(hvac\\.Client|hashicorp/vault|VAULT_(ADDR|TOKEN|NAMESPACE)|secretsmanager\\.|GetSecretValue|SecretManagerServiceClient|azure[._-]keyvault|@aws-sdk/client-secrets-manager)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI02", + "description": "Tool-call signing (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.js", + "*.java", + "*.kt", + "*.go", + "*.env*", + "*.yaml", + "*.yml", + "*.json", + "*.toml", + "*.cfg" + ], + "framework": "dsgai-2026-v1.0", + "id": "P02.8", + "name": "tool-call-signing", + "pcre": "(hmac|sign_request|verify_signature|tool_auth|mtls|mutual_tls)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI03", + "description": "Third-party LLM endpoints", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P03.1", + "name": "third-party-llm-endpoints", + "pcre": "(api\\.openai\\.com|api\\.anthropic\\.com|generativelanguage\\.googleapis\\.com|api\\.cohere\\.com|api\\.together\\.xyz|api\\.mistral\\.ai|api\\.groq\\.com)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI03", + "description": "Internal LLM gateway (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P03.2", + "name": "internal-llm-gateway", + "pcre": "(llm[._-]?gateway|ai[._-]?proxy|model[._-]?gateway|llm[._-]?proxy)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI03", + "description": "DLP / classification check", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.go", + "*.yaml", + "*.json", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P03.3", + "name": "dlp-classification-check", + "pcre": "(dlp|data_classification|classify_data|sensitivity_check)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "Unsafe pickle (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.1", + "name": "unsafe-pickle", + "pcre": "torch\\.load\\s*\\(", + "signal": "fail", + "subtract": [ + "P04.2" + ] + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "Safe pickle (PASS counter)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.2", + "name": "safe-pickle", + "pcre": "torch\\.load\\s*\\([^)]*weights_only\\s*=\\s*True", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "Artifact verification (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.3", + "name": "artifact-verification", + "pcre": "(sha256|verify_signature|model_hash|check_integrity)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI04", + "description": "Unpinned ML deps (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.4", + "name": "unpinned-ml-deps", + "pcre": "^(torch|transformers|tensorflow|langchain|openai|anthropic|llama-index)\\s*(>=|~=|\\^|>|<|<=|\\*|latest)", + "signal": "warn" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "SBOM in CI (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.5", + "name": "sbom-in-ci", + "pcre": "(syft|cyclonedx|spdx|sbom)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "Trusted registry (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.6", + "name": "trusted-registry", + "pcre": "(index-url|extra-index-url|artifactory|jfrog|verdaccio)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI04", + "description": "Hash-pinned install (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.sh", + "*.yml", + "*.yaml", + "Dockerfile", + "requirements*.txt", + "pyproject.toml", + "setup.py" + ], + "framework": "dsgai-2026-v1.0", + "id": "P04.7", + "name": "hash-pinned-install", + "pcre": "(--require-hashes|integrity\\s*:\\s*sha)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI05", + "description": "Document ingestion calls", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.1", + "name": "document-ingestion-calls", + "pcre": "(loader\\.load\\(|ingest_document\\(|add_documents\\(|index_document\\(|UnstructuredFileLoader|PyPDFLoader|DirectoryLoader)", + "requires_nearby": { + "rules": [ + "P05.2", + "P05.3" + ], + "scope": "module" + }, + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI05", + "description": "Access control (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.2", + "name": "access-control", + "pcre": "(acl_filter|access_check|permitted_docs|filter\\s*=.*tenant|namespace\\s*=.*tenant)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI05", + "description": "Tenant filter on search (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.3", + "name": "tenant-filter-on-search", + "pcre": "(similarity_search|vector_search)[^)]{0,100}(filter|namespace|where)[^)]{0,40}tenant", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI05", + "description": "Integrity check on docs (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.4", + "name": "integrity-check-on-docs", + "pcre": "(hashlib|sha256[^\\n]{0,40}doc|integrity[._-]check|verify[._-]document)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI05", + "description": "Chunk size limits (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.5", + "name": "chunk-size-limits", + "pcre": "(max_chunk_size|chunk_size\\s*=|max_doc_size|content_limit)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI05", + "description": "Path traversal risk", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P05.6", + "name": "path-traversal-risk", + "pcre": "(open\\(.*\\.\\.|os\\.path\\.join\\(.*request\\.|Path\\(.*request\\.)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI06", + "description": "Insecure MCP transport (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.json", + "*.yaml", + "*.toml", + "*.py", + "*.ts", + "mcp.json", + "claude_desktop_config.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P06.1", + "name": "insecure-mcp-transport", + "pcre": "(\"url\"\\s*:\\s*\"http://|transport[^\\n]{0,20}http://|\"command\"[^}]{0,200}--http(?!s))", + "signal": "fail" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI06", + "description": "MCP auth (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.json", + "*.yaml", + "*.toml", + "*.py", + "*.ts", + "mcp.json", + "claude_desktop_config.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P06.2", + "name": "mcp-auth", + "pcre": "(mcp[^\\n]{0,30}(api_key|auth|bearer)|x-api-key[^\\n]{0,20}mcp)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI06", + "description": "Tool schema validation (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.json", + "*.yaml", + "*.toml", + "*.py", + "*.ts", + "mcp.json", + "claude_desktop_config.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P06.3", + "name": "tool-schema-validation", + "pcre": "(jsonschema|pydantic[^\\n]{0,30}validate|zod|schema[^\\n]{0,20}tool|input_schema)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI06", + "description": "Wildcard tool perms (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.json", + "*.yaml", + "*.toml", + "*.py", + "*.ts", + "mcp.json", + "claude_desktop_config.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P06.4", + "name": "wildcard-tool-perms", + "pcre": "(tools\\s*[:=]\\s*\\*|\"tools\"\\s*:\\s*\"\\*\"|allow_all_tools)", + "signal": "warn" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI06", + "description": "uvicorn bind-all + no auth", + "exclude_globs": [], + "file_globs": [ + "*.json", + "*.yaml", + "*.toml", + "*.py", + "*.ts", + "mcp.json", + "claude_desktop_config.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P06.5", + "name": "uvicorn-bind-all-no-auth", + "pcre": "uvicorn\\.run\\([^)]*host\\s*=\\s*[\"']0\\.0\\.0\\.0", + "requires_nearby": { + "absent": true, + "rule": "P06.2", + "scope": "module" + }, + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI07", + "description": "TTL config", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P07.1", + "name": "ttl-config", + "notes": "Absence of P07.1-P07.4 in a multi-tenant or PII-handling repo = WARN.", + "pcre": "(ttl\\s*=|expires_in\\s*=|max_age\\s*=|RETENTION_DAYS|DATA_TTL|HISTORY_TTL)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI07", + "description": "Session cleanup (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P07.2", + "name": "session-cleanup", + "pcre": "(delete_session|clear_history|purge_conversation|clear_memory|delete_conversation)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI07", + "description": "Vector delete (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P07.3", + "name": "vector-delete", + "pcre": "(delete_namespace|delete_collection|drop_index|delete_index|reset_collection)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI07", + "description": "Right-to-erasure (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P07.4", + "name": "right-to-erasure", + "pcre": "(gdpr_delete|erase_user_data|handle_deletion|right_to_erasure|forget_user)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "DPIA / privacy assessment doc", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.1", + "name": "dpia-privacy-assessment-doc", + "notes": "Absence of P08.1-P08.6 in a production GenAI service = WARN; absence in a high-risk EU AI Act use case = FAIL.", + "pcre": "(DPIA|PIA|privacy_assessment|privacy[._-]impact)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "Data processing agreement ref", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.2", + "name": "data-processing-agreement-ref", + "pcre": "(data_processing_agreement|DPA|sub_processor)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "Consent capture", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.3", + "name": "consent-capture", + "pcre": "(consent_capture|capture_consent|consent_record|lawful_basis)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "EU AI Act annotation", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.4", + "name": "eu-ai-act-annotation", + "pcre": "(ai_act_risk_level|high_risk_ai|limited_risk|minimal_risk)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "Audit logging", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.5", + "name": "audit-logging", + "pcre": "(audit_log|decision_log|audit_trail)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI08", + "description": "Do-not-track honored", + "exclude_globs": [], + "file_globs": [ + "*.md", + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.json", + "docs/**", + "appsec/**", + "security-review/**" + ], + "framework": "dsgai-2026-v1.0", + "id": "P08.6", + "name": "do-not-track-honored", + "pcre": "(do_not_track|opt_out|DNT)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI09", + "description": "EXIF / metadata strip", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "multimodal", + "id": "P09.1", + "name": "exif-metadata-strip", + "pcre": "(strip_exif|remove_metadata|PIL\\.Image|exifread|piexif)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI09", + "description": "Image PII / moderation", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "multimodal", + "id": "P09.2", + "name": "image-pii-moderation", + "pcre": "(detect_pii_in_image|content_filter|nsfw_detect|moderate_image)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI09", + "description": "MIME / file type validation", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "multimodal", + "id": "P09.3", + "name": "mime-file-type-validation", + "pcre": "(allowed_types|mime_type_check|magic\\.from_buffer|filetype\\.guess)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI09", + "description": "Size limit on upload", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "multimodal", + "id": "P09.4", + "name": "size-limit-on-upload", + "pcre": "(max_upload_size|MAX_FILE_SIZE|content[._-]length[._-]limit)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI09", + "description": "Steganography detection", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "multimodal", + "id": "P09.5", + "name": "steganography-detection", + "notes": "Absence = note only (advanced control); P09.1-P09.4 absence in a multimodal pipeline = WARN.", + "pcre": "(stego|steganography|stegano|lsb_detect)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI10", + "description": "Membership inference test", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.r" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "synthetic_data", + "id": "P10.1", + "name": "membership-inference-test", + "notes": "Synthetic data pipeline without any of P10.1/P10.2/P10.4 = FAIL.", + "pcre": "(membership_inference|mia_test|mia_attack)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI10", + "description": "Anonymity validation", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.r" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "synthetic_data", + "id": "P10.2", + "name": "anonymity-validation", + "pcre": "(k_anonymity|l_diversity|t_closeness|anonymization_check)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI10", + "description": "Privacy-preserving lib", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.r" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "synthetic_data", + "id": "P10.3", + "name": "privacy-preserving-lib", + "pcre": "(SDV|gretel|synthetic_data_vault|smartnoise)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI10", + "description": "DP noise in generation", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.r" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "synthetic_data", + "id": "P10.4", + "name": "dp-noise-in-generation", + "pcre": "(epsilon\\s*=|noise_multiplier\\s*=|dp_noise|laplace_noise|gaussian_noise)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI10", + "description": "Re-identification risk note", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.r" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "synthetic_data", + "id": "P10.5", + "name": "re-identification-risk-note", + "pcre": "(reidentification_risk|reid_check|disclosure_risk)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI11", + "description": "Vector query call sites", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P11.1", + "name": "vector-query-call-sites", + "pcre": "(similarity_search|max_marginal_relevance_search|as_retriever|(vectorstore|vector_store|retriever|index|collection|client)\\.(query|search)\\()", + "requires_nearby": { + "lines": 15, + "rule": "P11.2" + }, + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI11", + "description": "Tenant filter present (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P11.2", + "name": "tenant-filter-present", + "pcre": "(namespace\\s*=[^,)]{0,40}tenant|collection[^=]{0,20}=\\s*f?[\"'][^\"']*\\{?tenant|filter\\s*=\\s*\\{[^}]*tenant)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI11", + "description": "Tenant verification (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P11.3", + "name": "tenant-verification", + "pcre": "(tenant_id\\s*in\\s*session|verify_tenant|assert_tenant|check_tenant|require_tenant)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI11", + "description": "Cross-tenant cache risk", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P11.4", + "name": "cross-tenant-cache-risk", + "pcre": "(global[._-]cache|shared[._-]prompt[._-]cache|@cache(?!\\s*\\(.*tenant))", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "Raw LLM-output execution (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.1", + "name": "raw-llm-output-execution", + "pcre": "execute\\([^)]*(llm|response|completion|output|generated)", + "signal": "fail" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "LangChain SQL agent (FAIL signal)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.2", + "name": "langchain-sql-agent", + "pcre": "(SQLDatabaseChain|create_sql_agent|SQLDatabaseToolkit)", + "signal": "fail" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "Parameterized query (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.3", + "name": "parameterized-query", + "pcre": "(cursor\\.execute\\([^)]+,\\s*[\\[\\(]|prepareStatement|bindValue|:param)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "Read-only conn (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.4", + "name": "read-only-conn", + "pcre": "(read_only\\s*=\\s*True|readonly\\s*=\\s*True|read_only_connection|default_transaction_read_only)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "Query validator (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.5", + "name": "query-validator", + "pcre": "(validate_query|query_validator|safe_sql|sql_guard|sanitize_sql)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "DDL from agent (FAIL)", + "exclude_globs": [ + "**/migrations/**", + "**/fixtures/**", + "**/tests/**", + "**/test/**" + ], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.6", + "name": "ddl-from-agent", + "notes": "DDL in migrations/fixtures/tests is benign — excluded from FAIL.", + "pcre": "\\b(DROP\\s+TABLE|TRUNCATE|ALTER\\s+TABLE|DELETE\\s+FROM)\\b", + "signal": "fail" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI12", + "description": "Row limit (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P12.7", + "name": "row-limit", + "pcre": "(LIMIT\\s+\\d+|max_rows\\s*=|fetch_limit\\s*=)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI13", + "description": "Vector store auth configured", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P13.1", + "name": "vector-store-auth-configured", + "pcre": "(CHROMA_SERVER_AUTH|QDRANT_API_KEY|PINECONE_API_KEY|WEAVIATE_API_KEY|MILVUS_TOKEN|vectorstore[^\\n]{0,30}api_key)", + "signal": "info" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI13", + "description": "TLS endpoints (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P13.2", + "name": "tls-endpoints", + "pcre": "(https://[^\"']*?(qdrant|weaviate|pinecone|chroma)|ssl\\s*=\\s*True|tls\\s*=\\s*True|grpcs://)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI13", + "description": "Insecure binding (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P13.3", + "name": "insecure-binding", + "pcre": "(host[^\\n]{0,15}0\\.0\\.0\\.0|ALLOW_RESET\\s*=\\s*True|chroma[^\\n]{0,30}http://localhost|qdrant[^\\n]{0,30}http://localhost)", + "signal": "warn" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI13", + "description": "Hardcoded vector token (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P13.4", + "name": "hardcoded-vector-token", + "pcre": "(QDRANT_API_KEY|PINECONE_API_KEY|WEAVIATE_API_KEY)\\s*=\\s*[\"'][A-Za-z0-9_\\-]{16,}", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI14", + "description": "Prompt logging disabled (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P14.1", + "name": "prompt-logging-disabled", + "pcre": "(log_prompts\\s*=\\s*False|capture_content\\s*=\\s*False|OTEL_LOG_PROMPTS[^\\n]{0,10}false|log_completions\\s*=\\s*False)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI14", + "description": "Prompt logging enabled (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P14.2", + "name": "prompt-logging-enabled", + "pcre": "(log_prompts\\s*=\\s*True|capture_content\\s*=\\s*True|log_full_response\\s*=\\s*True)", + "signal": "warn" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI14", + "description": "PII redaction in log (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P14.3", + "name": "pii-redaction-in-log", + "pcre": "(redact_pii|mask_sensitive|sanitize_log|scrub[^\\n]{0,10}log|log[^\\n]{0,10}redact)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI14", + "description": "Raw response print (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P14.4", + "name": "raw-response-print", + "notes": "May contain inline PII in the format string — treated as VALUE-BEARING.", + "pcre": "(print\\((response|completion)|console\\.log\\((response|completion)|logger\\.(debug|info)\\([^)]*(response|completion))", + "signal": "warn" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI14", + "description": "LangSmith / Langfuse config", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.yaml", + "*.env*" + ], + "framework": "dsgai-2026-v1.0", + "id": "P14.5", + "name": "langsmith-langfuse-config", + "pcre": "(LANGSMITH_API_KEY|LANGFUSE_PUBLIC_KEY|langfuse[^\\n]{0,20}init|langsmith[^\\n]{0,20}trace)", + "signal": "info" + }, + { + "classification": "value_bearing", + "confidence": "high", + "control": "DSGAI15", + "description": "Secret in system prompt (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P15.1", + "name": "secret-in-system-prompt", + "pcre": "(system_prompt|system_message)\\s*=\\s*[\"'][^\"']*(api_key|password|secret|token|sk-)", + "signal": "fail" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI15", + "description": "Context size limit (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P15.2", + "name": "context-size-limit", + "pcre": "(max_context_length\\s*=|context_window_limit\\s*=|max_context_tokens\\s*=|truncate_context)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "medium", + "control": "DSGAI15", + "description": "Prompt from config (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P15.3", + "name": "prompt-from-config", + "pcre": "(system_prompt|system_message)[^\\n]{0,40}(config|vault|os\\.environ|getenv|secret_manager)", + "signal": "pass_signal" + }, + { + "classification": "value_bearing", + "confidence": "low", + "control": "DSGAI15", + "description": "Over-fetch aggregation (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P15.4", + "name": "over-fetch-aggregation", + "pcre": "(customer_360|user_profile_all|fetch_all_user_data|get_full_profile|select\\s*\\*\\s*from\\s+users)", + "signal": "warn" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI16", + "description": "AI-ignore file present (PASS)", + "exclude_globs": [], + "file_globs": [ + ".copilotignore", + ".aiignore", + ".cursorignore", + ".continueignore", + ".codeiumignore", + ".vscode/settings.json", + "cursor.json", + "continue.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P16.1", + "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)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI16", + "description": "Sensitive paths excluded", + "exclude_globs": [], + "file_globs": [ + ".copilotignore", + ".aiignore", + ".cursorignore", + ".continueignore", + ".codeiumignore", + ".vscode/settings.json", + "cursor.json", + "continue.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P16.2", + "name": "sensitive-paths-excluded", + "notes": "Matched inside any AI-ignore file.", + "pcre": "(\\.env|secrets|credentials|\\.aws|\\.ssh|private_key)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI16", + "description": "Telemetry off (PASS)", + "exclude_globs": [], + "file_globs": [ + ".copilotignore", + ".aiignore", + ".cursorignore", + ".continueignore", + ".codeiumignore", + ".vscode/settings.json", + "cursor.json", + "continue.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P16.3", + "name": "telemetry-off", + "pcre": "(telemetry[^\\n]{0,10}(off|false|disabled)|share_data\\s*[:=]\\s*false)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI16", + "description": "Context scope limits", + "exclude_globs": [], + "file_globs": [ + ".copilotignore", + ".aiignore", + ".cursorignore", + ".continueignore", + ".codeiumignore", + ".vscode/settings.json", + "cursor.json", + "continue.json" + ], + "framework": "dsgai-2026-v1.0", + "id": "P16.4", + "name": "context-scope-limits", + "pcre": "(contextWindow|ignorePaths|excludeFiles|maxContextLines)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Circuit breaker (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.1", + "name": "circuit-breaker", + "notes": "LLM-calling module with none of P17.1-P17.5 = WARN.", + "pcre": "(CircuitBreaker|@circuit|tenacity|resilience4j|pybreaker)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Retry with backoff (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.2", + "name": "retry-with-backoff", + "pcre": "(retry\\s*\\(|@retry|exponential_backoff|max_retries\\s*=|backoff_factor\\s*=|retry_with_backoff)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Timeout on LLM call (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.3", + "name": "timeout-on-llm-call", + "pcre": "(timeout\\s*=\\s*\\d|request_timeout\\s*=|httpx\\.[^\\n]{0,30}timeout)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Rate limit incoming (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.4", + "name": "rate-limit-incoming", + "pcre": "(rate_limit|@throttle|RateLimiter|slowapi|flask_limiter|express-rate-limit)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Fallback response (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.5", + "name": "fallback-response", + "pcre": "(fallback_response|default_response|graceful_degradation|on_failure_return)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI17", + "description": "Unbounded retry (FAIL)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P17.6", + "name": "unbounded-retry", + "pcre": "while\\s+True[^}]{0,200}(generate|complete|chat)|retry\\s*\\(\\s*\\)", + "signal": "fail" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI18", + "description": "Output guardrails (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P18.1", + "name": "output-guardrails", + "pcre": "(guardrails|nemo[._-]guardrails|llm_guard|output_guard|filter_output|sanitize_response|moderation)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI18", + "description": "PII detection on output", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P18.2", + "name": "pii-detection-on-output", + "pcre": "(detect_pii[^\\n]{0,20}output|output[^\\n]{0,20}detect_pii|presidio[^\\n]{0,20}output|scan_output)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI18", + "description": "Logprobs exposed (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P18.3", + "name": "logprobs-exposed", + "pcre": "(logprobs\\s*=\\s*True|return_logprobs\\s*=\\s*True|include_logprobs\\s*=\\s*True|top_logprobs\\s*=\\s*\\d)", + "signal": "warn" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI18", + "description": "LLM call sites (count)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P18.4", + "name": "llm-call-sites", + "pcre": "(\\.chat\\.completions\\.create|client\\.messages\\.create|openai\\.ChatCompletion|anthropic\\.messages|generate_content|chat\\.complete)", + "requires_nearby": { + "lines": 10, + "rule": "P18.5" + }, + "signal": "count" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI18", + "description": "max_tokens set (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P18.5", + "name": "max-tokens-set", + "pcre": "max_tokens\\s*=\\s*\\d+", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI19", + "description": "Anonymization before export", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.ts", + "*.yaml" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "labeling", + "id": "P19.1", + "name": "anonymization-before-export", + "pcre": "(anonymize_for_labeling|pseudonymize|redact_before_export|mask_for_labeling)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI19", + "description": "Labeling SDK auth from vault", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.ts", + "*.yaml" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "labeling", + "id": "P19.2", + "name": "labeling-sdk-auth-from-vault", + "pcre": "(label_studio[^\\n]{0,30}vault|labelbox[^\\n]{0,30}secret|scale_api_key[^\\n]{0,30}(vault|env))", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI19", + "description": "Labeling access audit", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.ts", + "*.yaml" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "labeling", + "id": "P19.3", + "name": "labeling-access-audit", + "pcre": "(label[._-]audit|labeling_access_log|annotator_audit)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI19", + "description": "Re-identification post-label", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ipynb", + "*.ts", + "*.yaml" + ], + "framework": "dsgai-2026-v1.0", + "gated_on": "labeling", + "id": "P19.4", + "name": "re-identification-post-label", + "pcre": "(reid_check_post_label|labeling_reidentification|post_label_validation)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI20", + "description": "API auth required (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P20.1", + "name": "api-auth-required", + "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)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI20", + "description": "Rate limit (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P20.2", + "name": "rate-limit", + "pcre": "(rate_limit|RateLimiter|@throttle|slowapi|flask_limiter|express-rate-limit|@limiter\\.limit)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI20", + "description": "Input length validation (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P20.3", + "name": "input-length-validation", + "pcre": "(max_input_length\\s*=|max_input_tokens\\s*=|input[._-]truncat|validate[._-]length)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI20", + "description": "Prompt injection detect (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P20.4", + "name": "prompt-injection-detect", + "pcre": "(prompt_injection_detect|detect_injection|llm_guard|rebuff|input_guard|nemo[._-]guard)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI20", + "description": "Inference endpoint (count)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt", + "*.go" + ], + "framework": "dsgai-2026-v1.0", + "id": "P20.5", + "name": "inference-endpoint", + "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))", + "requires_nearby": { + "lines": 15, + "rules": [ + "P20.1", + "P20.2" + ] + }, + "signal": "count" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI21", + "description": "Read-only retrieval (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P21.1", + "name": "read-only-retrieval", + "pcre": "(read_only\\s*=\\s*True|readonly\\s*=\\s*True|HttpMethod\\.GET|http_method[^\\n]{0,10}get)", + "signal": "pass_signal" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI21", + "description": "KB write from agent (WARN)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P21.2", + "name": "kb-write-from-agent", + "notes": "P21.2 in an agent module without P21.3 = WARN.", + "pcre": "(chromadb|qdrant|pinecone|weaviate|knowledge_base|kb_client)[^\\n]{0,40}\\.(upsert|insert|update|delete|add_documents|write)\\(", + "signal": "warn" + }, + { + "classification": "structural", + "confidence": "low", + "control": "DSGAI21", + "description": "Write content validation", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P21.3", + "name": "write-content-validation", + "pcre": "(validate_content|sanitize[._-]write|verify[._-]knowledge|content[._-]check[._-]write)", + "signal": "info" + }, + { + "classification": "structural", + "confidence": "medium", + "control": "DSGAI21", + "description": "KB versioning (PASS)", + "exclude_globs": [], + "file_globs": [ + "*.py", + "*.ts", + "*.java", + "*.kt" + ], + "framework": "dsgai-2026-v1.0", + "id": "P21.4", + "name": "kb-versioning", + "pcre": "(versioned\\s*=\\s*True|audit_writes|version_id|kb_audit|knowledge[._-]audit)", + "signal": "pass_signal" + } + ], + "ruleset_version": "0.3.0" +} diff --git a/dsgai_scanner_tool/rules/dsgai-rules.yaml b/dsgai_scanner_tool/rules/dsgai-rules.yaml new file mode 100644 index 0000000..6816ea4 --- /dev/null +++ b/dsgai_scanner_tool/rules/dsgai-rules.yaml @@ -0,0 +1,1202 @@ +# DSGAI scanner detection rules — source of truth. +# Generated once from dsgai_scanner_tool.md Step 2 by build/generate_rules.py, +# then hand-maintained. Validated by rules/rules.schema.json (see rules/README.md). +ruleset_version: '0.3.0' +framework: 'dsgai-2026-v1.0' +rules: + - id: P01.1 + control: DSGAI01 + name: pii-scrubbing-imports + classification: structural + signal: info + confidence: low + pcre: '(anonymize|redact|scrub_pii|piiDetect|mask_pii|presidio)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'PII scrubbing imports' + - id: P01.2 + control: DSGAI01 + name: no-train-flags + classification: structural + signal: info + confidence: low + pcre: '(allow_training|training_opt_out|X-Training-Data|no_train)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'No-train flags' + - id: P01.3 + control: DSGAI01 + name: output-pii-filter + classification: structural + signal: info + confidence: low + pcre: '(filter_pii|remove_pii|output_sanitize|output_filter)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Output PII filter' + - id: P01.4 + control: DSGAI01 + name: differential-privacy-lib + classification: structural + signal: info + confidence: low + pcre: '(opacus|tensorflow\.privacy|dp-accounting|differential\.privacy)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Differential privacy lib' + - id: P01.5 + control: DSGAI01 + name: consent-gdpr-check + classification: structural + signal: info + confidence: low + pcre: '(consent_capture|gdpr|ccpa|data_subject|lawful_basis)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Consent / GDPR check' + - id: P02.1 + control: DSGAI02 + name: hardcoded-llm-api-key + classification: value_bearing + signal: fail + confidence: high + pcre: '(?i)(OPENAI_API_KEY|openai[._-]?api[._-]?key)\s*[:=]\s*["'']sk-[A-Za-z0-9_\-]{20,}' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded LLM API key (OpenAI)' + - id: P02.2 + control: DSGAI02 + name: hardcoded-llm-api-key + classification: value_bearing + signal: fail + confidence: high + pcre: '(?i)(ANTHROPIC_API_KEY|anthropic[._-]?api[._-]?key)\s*[:=]\s*["'']sk-ant-[A-Za-z0-9_\-]{20,}' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded LLM API key (Anthropic)' + - id: P02.3 + control: DSGAI02 + name: hardcoded-cohere-google-hf-tokens + classification: value_bearing + signal: fail + confidence: high + pcre: '(?i)(COHERE_API_KEY|GOOGLE_API_KEY|HF_TOKEN|HUGGINGFACE_TOKEN)\s*[:=]\s*["''][A-Za-z0-9_\-]{20,}' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded Cohere/Google/HF tokens' + - id: P02.4 + control: DSGAI02 + name: hardcoded-aws-creds + classification: value_bearing + signal: fail + confidence: high + pcre: '(AWS_ACCESS_KEY_ID|aws_access_key_id)\s*[:=]\s*["''][A-Z0-9]{16,}' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded AWS creds' + - id: P02.5 + control: DSGAI02 + name: hardcoded-azure-gcp-cred + classification: value_bearing + signal: fail + confidence: high + pcre: '(AZURE_OPENAI_KEY|GCP_SERVICE_ACCOUNT_KEY)\s*[:=]\s*["''][A-Za-z0-9_\-]{16,}' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded Azure/GCP cred' + - id: P02.6 + control: DSGAI02 + name: wildcard-token-scope + classification: value_bearing + signal: warn + confidence: low + pcre: '("scope"\s*:\s*"\*|permissions[^\n]{0,30}\*|scope[^\n]{0,20}admin)' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Wildcard token scope (warn)' + - id: P02.7 + control: DSGAI02 + name: vault-secrets-manager + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(hvac\.Client|hashicorp/vault|VAULT_(ADDR|TOKEN|NAMESPACE)|secretsmanager\.|GetSecretValue|SecretManagerServiceClient|azure[._-]keyvault|@aws-sdk/client-secrets-manager)' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Vault/secrets-manager (PASS)' + - id: P02.8 + control: DSGAI02 + name: tool-call-signing + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(hmac|sign_request|verify_signature|tool_auth|mtls|mutual_tls)' + file_globs: ['*.py', '*.ts', '*.js', '*.java', '*.kt', '*.go', '*.env*', '*.yaml', '*.yml', '*.json', '*.toml', '*.cfg'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Tool-call signing (PASS)' + - id: P03.1 + control: DSGAI03 + name: third-party-llm-endpoints + classification: structural + signal: info + confidence: low + pcre: '(api\.openai\.com|api\.anthropic\.com|generativelanguage\.googleapis\.com|api\.cohere\.com|api\.together\.xyz|api\.mistral\.ai|api\.groq\.com)' + file_globs: ['*.py', '*.ts', '*.java', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Third-party LLM endpoints' + - id: P03.2 + control: DSGAI03 + name: internal-llm-gateway + classification: structural + signal: pass_signal + confidence: medium + pcre: '(llm[._-]?gateway|ai[._-]?proxy|model[._-]?gateway|llm[._-]?proxy)' + file_globs: ['*.py', '*.ts', '*.java', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Internal LLM gateway (PASS)' + - id: P03.3 + control: DSGAI03 + name: dlp-classification-check + classification: structural + signal: info + confidence: low + pcre: '(dlp|data_classification|classify_data|sensitivity_check)' + file_globs: ['*.py', '*.ts', '*.java', '*.go', '*.yaml', '*.json', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'DLP / classification check' + - id: P04.1 + control: DSGAI04 + name: unsafe-pickle + classification: structural + signal: fail + confidence: medium + pcre: 'torch\.load\s*\(' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Unsafe pickle (FAIL)' + subtract: ['P04.2'] + - id: P04.2 + control: DSGAI04 + name: safe-pickle + classification: structural + signal: pass_signal + confidence: medium + pcre: 'torch\.load\s*\([^)]*weights_only\s*=\s*True' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Safe pickle (PASS counter)' + - id: P04.3 + control: DSGAI04 + name: artifact-verification + classification: structural + signal: pass_signal + confidence: medium + pcre: '(sha256|verify_signature|model_hash|check_integrity)' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Artifact verification (PASS)' + - id: P04.4 + control: DSGAI04 + name: unpinned-ml-deps + classification: structural + signal: warn + confidence: low + pcre: '^(torch|transformers|tensorflow|langchain|openai|anthropic|llama-index)\s*(>=|~=|\^|>|<|<=|\*|latest)' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Unpinned ML deps (WARN)' + - id: P04.5 + control: DSGAI04 + name: sbom-in-ci + classification: structural + signal: pass_signal + confidence: medium + pcre: '(syft|cyclonedx|spdx|sbom)' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'SBOM in CI (PASS)' + - id: P04.6 + control: DSGAI04 + name: trusted-registry + classification: structural + signal: pass_signal + confidence: medium + pcre: '(index-url|extra-index-url|artifactory|jfrog|verdaccio)' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Trusted registry (PASS)' + - id: P04.7 + control: DSGAI04 + name: hash-pinned-install + classification: structural + signal: pass_signal + confidence: medium + pcre: '(--require-hashes|integrity\s*:\s*sha)' + file_globs: ['*.py', '*.sh', '*.yml', '*.yaml', 'Dockerfile', 'requirements*.txt', 'pyproject.toml', 'setup.py'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hash-pinned install (PASS)' + - id: P05.1 + control: DSGAI05 + name: document-ingestion-calls + classification: structural + signal: info + confidence: low + pcre: '(loader\.load\(|ingest_document\(|add_documents\(|index_document\(|UnstructuredFileLoader|PyPDFLoader|DirectoryLoader)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Document ingestion calls' + requires_nearby: {rules: ['P05.2', 'P05.3'], scope: module} + - id: P05.2 + control: DSGAI05 + name: access-control + classification: structural + signal: pass_signal + confidence: medium + pcre: '(acl_filter|access_check|permitted_docs|filter\s*=.*tenant|namespace\s*=.*tenant)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Access control (PASS)' + - id: P05.3 + control: DSGAI05 + name: tenant-filter-on-search + classification: structural + signal: pass_signal + confidence: medium + pcre: '(similarity_search|vector_search)[^)]{0,100}(filter|namespace|where)[^)]{0,40}tenant' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Tenant filter on search (PASS)' + - id: P05.4 + control: DSGAI05 + name: integrity-check-on-docs + classification: structural + signal: pass_signal + confidence: medium + pcre: '(hashlib|sha256[^\n]{0,40}doc|integrity[._-]check|verify[._-]document)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Integrity check on docs (PASS)' + - id: P05.5 + control: DSGAI05 + name: chunk-size-limits + classification: structural + signal: pass_signal + confidence: medium + pcre: '(max_chunk_size|chunk_size\s*=|max_doc_size|content_limit)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Chunk size limits (PASS)' + - id: P05.6 + control: DSGAI05 + name: path-traversal-risk + classification: structural + signal: info + confidence: low + pcre: '(open\(.*\.\.|os\.path\.join\(.*request\.|Path\(.*request\.)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Path traversal risk' + - id: P06.1 + control: DSGAI06 + name: insecure-mcp-transport + classification: structural + signal: fail + confidence: medium + pcre: '("url"\s*:\s*"http://|transport[^\n]{0,20}http://|"command"[^}]{0,200}--http(?!s))' + file_globs: ['*.json', '*.yaml', '*.toml', '*.py', '*.ts', 'mcp.json', 'claude_desktop_config.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Insecure MCP transport (FAIL)' + - id: P06.2 + control: DSGAI06 + name: mcp-auth + classification: structural + signal: pass_signal + confidence: medium + pcre: '(mcp[^\n]{0,30}(api_key|auth|bearer)|x-api-key[^\n]{0,20}mcp)' + file_globs: ['*.json', '*.yaml', '*.toml', '*.py', '*.ts', 'mcp.json', 'claude_desktop_config.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'MCP auth (PASS)' + - id: P06.3 + control: DSGAI06 + name: tool-schema-validation + classification: structural + signal: pass_signal + confidence: medium + pcre: '(jsonschema|pydantic[^\n]{0,30}validate|zod|schema[^\n]{0,20}tool|input_schema)' + file_globs: ['*.json', '*.yaml', '*.toml', '*.py', '*.ts', 'mcp.json', 'claude_desktop_config.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Tool schema validation (PASS)' + - id: P06.4 + control: DSGAI06 + name: wildcard-tool-perms + classification: structural + signal: warn + confidence: low + pcre: '(tools\s*[:=]\s*\*|"tools"\s*:\s*"\*"|allow_all_tools)' + file_globs: ['*.json', '*.yaml', '*.toml', '*.py', '*.ts', 'mcp.json', 'claude_desktop_config.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Wildcard tool perms (WARN)' + - id: P06.5 + control: DSGAI06 + name: uvicorn-bind-all-no-auth + classification: structural + signal: info + confidence: low + pcre: 'uvicorn\.run\([^)]*host\s*=\s*["'']0\.0\.0\.0' + file_globs: ['*.json', '*.yaml', '*.toml', '*.py', '*.ts', 'mcp.json', 'claude_desktop_config.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'uvicorn bind-all + no auth' + requires_nearby: {rule: P06.2, scope: module, absent: true} + - id: P07.1 + control: DSGAI07 + name: ttl-config + classification: structural + signal: info + confidence: low + pcre: '(ttl\s*=|expires_in\s*=|max_age\s*=|RETENTION_DAYS|DATA_TTL|HISTORY_TTL)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'TTL config' + notes: 'Absence of P07.1-P07.4 in a multi-tenant or PII-handling repo = WARN.' + - id: P07.2 + control: DSGAI07 + name: session-cleanup + classification: structural + signal: pass_signal + confidence: medium + pcre: '(delete_session|clear_history|purge_conversation|clear_memory|delete_conversation)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Session cleanup (PASS)' + - id: P07.3 + control: DSGAI07 + name: vector-delete + classification: structural + signal: pass_signal + confidence: medium + pcre: '(delete_namespace|delete_collection|drop_index|delete_index|reset_collection)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Vector delete (PASS)' + - id: P07.4 + control: DSGAI07 + name: right-to-erasure + classification: structural + signal: pass_signal + confidence: medium + pcre: '(gdpr_delete|erase_user_data|handle_deletion|right_to_erasure|forget_user)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Right-to-erasure (PASS)' + - id: P08.1 + control: DSGAI08 + name: dpia-privacy-assessment-doc + classification: structural + signal: info + confidence: low + pcre: '(DPIA|PIA|privacy_assessment|privacy[._-]impact)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'DPIA / privacy assessment doc' + notes: 'Absence of P08.1-P08.6 in a production GenAI service = WARN; absence in a high-risk EU AI Act use case = FAIL.' + - id: P08.2 + control: DSGAI08 + name: data-processing-agreement-ref + classification: structural + signal: info + confidence: low + pcre: '(data_processing_agreement|DPA|sub_processor)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Data processing agreement ref' + - id: P08.3 + control: DSGAI08 + name: consent-capture + classification: structural + signal: info + confidence: low + pcre: '(consent_capture|capture_consent|consent_record|lawful_basis)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Consent capture' + - id: P08.4 + control: DSGAI08 + name: eu-ai-act-annotation + classification: structural + signal: info + confidence: low + pcre: '(ai_act_risk_level|high_risk_ai|limited_risk|minimal_risk)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'EU AI Act annotation' + - id: P08.5 + control: DSGAI08 + name: audit-logging + classification: structural + signal: info + confidence: low + pcre: '(audit_log|decision_log|audit_trail)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Audit logging' + - id: P08.6 + control: DSGAI08 + name: do-not-track-honored + classification: structural + signal: info + confidence: low + pcre: '(do_not_track|opt_out|DNT)' + file_globs: ['*.md', '*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.json', 'docs/**', 'appsec/**', 'security-review/**'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Do-not-track honored' + - id: P09.1 + control: DSGAI09 + name: exif-metadata-strip + classification: structural + signal: info + confidence: low + pcre: '(strip_exif|remove_metadata|PIL\.Image|exifread|piexif)' + file_globs: ['*.py', '*.ts', '*.java'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'EXIF / metadata strip' + gated_on: multimodal + - id: P09.2 + control: DSGAI09 + name: image-pii-moderation + classification: structural + signal: info + confidence: low + pcre: '(detect_pii_in_image|content_filter|nsfw_detect|moderate_image)' + file_globs: ['*.py', '*.ts', '*.java'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Image PII / moderation' + gated_on: multimodal + - id: P09.3 + control: DSGAI09 + name: mime-file-type-validation + classification: structural + signal: info + confidence: low + pcre: '(allowed_types|mime_type_check|magic\.from_buffer|filetype\.guess)' + file_globs: ['*.py', '*.ts', '*.java'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'MIME / file type validation' + gated_on: multimodal + - id: P09.4 + control: DSGAI09 + name: size-limit-on-upload + classification: structural + signal: info + confidence: low + pcre: '(max_upload_size|MAX_FILE_SIZE|content[._-]length[._-]limit)' + file_globs: ['*.py', '*.ts', '*.java'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Size limit on upload' + gated_on: multimodal + - id: P09.5 + control: DSGAI09 + name: steganography-detection + classification: structural + signal: info + confidence: low + pcre: '(stego|steganography|stegano|lsb_detect)' + file_globs: ['*.py', '*.ts', '*.java'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Steganography detection' + gated_on: multimodal + notes: 'Absence = note only (advanced control); P09.1-P09.4 absence in a multimodal pipeline = WARN.' + - id: P10.1 + control: DSGAI10 + name: membership-inference-test + classification: structural + signal: info + confidence: low + pcre: '(membership_inference|mia_test|mia_attack)' + file_globs: ['*.py', '*.ipynb', '*.r'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Membership inference test' + gated_on: synthetic_data + notes: 'Synthetic data pipeline without any of P10.1/P10.2/P10.4 = FAIL.' + - id: P10.2 + control: DSGAI10 + name: anonymity-validation + classification: structural + signal: info + confidence: low + pcre: '(k_anonymity|l_diversity|t_closeness|anonymization_check)' + file_globs: ['*.py', '*.ipynb', '*.r'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Anonymity validation' + gated_on: synthetic_data + - id: P10.3 + control: DSGAI10 + name: privacy-preserving-lib + classification: structural + signal: info + confidence: low + pcre: '(SDV|gretel|synthetic_data_vault|smartnoise)' + file_globs: ['*.py', '*.ipynb', '*.r'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Privacy-preserving lib' + gated_on: synthetic_data + - id: P10.4 + control: DSGAI10 + name: dp-noise-in-generation + classification: structural + signal: info + confidence: low + pcre: '(epsilon\s*=|noise_multiplier\s*=|dp_noise|laplace_noise|gaussian_noise)' + file_globs: ['*.py', '*.ipynb', '*.r'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'DP noise in generation' + gated_on: synthetic_data + - id: P10.5 + control: DSGAI10 + name: re-identification-risk-note + classification: structural + signal: info + confidence: low + pcre: '(reidentification_risk|reid_check|disclosure_risk)' + file_globs: ['*.py', '*.ipynb', '*.r'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Re-identification risk note' + gated_on: synthetic_data + - id: P11.1 + control: DSGAI11 + name: vector-query-call-sites + classification: structural + signal: info + confidence: low + pcre: '(similarity_search|max_marginal_relevance_search|as_retriever|(vectorstore|vector_store|retriever|index|collection|client)\.(query|search)\()' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Vector query call sites' + requires_nearby: {rule: P11.2, lines: 15} + - id: P11.2 + control: DSGAI11 + name: tenant-filter-present + classification: structural + signal: pass_signal + confidence: medium + pcre: '(namespace\s*=[^,)]{0,40}tenant|collection[^=]{0,20}=\s*f?["''][^"'']*\{?tenant|filter\s*=\s*\{[^}]*tenant)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Tenant filter present (PASS)' + - id: P11.3 + control: DSGAI11 + name: tenant-verification + classification: structural + signal: pass_signal + confidence: medium + pcre: '(tenant_id\s*in\s*session|verify_tenant|assert_tenant|check_tenant|require_tenant)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Tenant verification (PASS)' + - id: P11.4 + control: DSGAI11 + name: cross-tenant-cache-risk + classification: structural + signal: info + confidence: low + pcre: '(global[._-]cache|shared[._-]prompt[._-]cache|@cache(?!\s*\(.*tenant))' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Cross-tenant cache risk' + - id: P12.1 + control: DSGAI12 + name: raw-llm-output-execution + classification: structural + signal: fail + confidence: medium + pcre: 'execute\([^)]*(llm|response|completion|output|generated)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Raw LLM-output execution (FAIL)' + - id: P12.2 + control: DSGAI12 + name: langchain-sql-agent + classification: structural + signal: fail + confidence: medium + pcre: '(SQLDatabaseChain|create_sql_agent|SQLDatabaseToolkit)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'LangChain SQL agent (FAIL signal)' + - id: P12.3 + control: DSGAI12 + name: parameterized-query + classification: structural + signal: pass_signal + confidence: medium + pcre: '(cursor\.execute\([^)]+,\s*[\[\(]|prepareStatement|bindValue|:param)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Parameterized query (PASS)' + - id: P12.4 + control: DSGAI12 + name: read-only-conn + classification: structural + signal: pass_signal + confidence: medium + pcre: '(read_only\s*=\s*True|readonly\s*=\s*True|read_only_connection|default_transaction_read_only)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Read-only conn (PASS)' + - id: P12.5 + control: DSGAI12 + name: query-validator + classification: structural + signal: pass_signal + confidence: medium + pcre: '(validate_query|query_validator|safe_sql|sql_guard|sanitize_sql)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Query validator (PASS)' + - id: P12.6 + control: DSGAI12 + name: ddl-from-agent + classification: structural + signal: fail + confidence: medium + pcre: '\b(DROP\s+TABLE|TRUNCATE|ALTER\s+TABLE|DELETE\s+FROM)\b' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: ['**/migrations/**', '**/fixtures/**', '**/tests/**', '**/test/**'] + framework: 'dsgai-2026-v1.0' + description: 'DDL from agent (FAIL)' + notes: 'DDL in migrations/fixtures/tests is benign — excluded from FAIL.' + - id: P12.7 + control: DSGAI12 + name: row-limit + classification: structural + signal: pass_signal + confidence: medium + pcre: '(LIMIT\s+\d+|max_rows\s*=|fetch_limit\s*=)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Row limit (PASS)' + - id: P13.1 + control: DSGAI13 + name: vector-store-auth-configured + classification: value_bearing + signal: info + confidence: low + pcre: '(CHROMA_SERVER_AUTH|QDRANT_API_KEY|PINECONE_API_KEY|WEAVIATE_API_KEY|MILVUS_TOKEN|vectorstore[^\n]{0,30}api_key)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Vector store auth configured' + - id: P13.2 + control: DSGAI13 + name: tls-endpoints + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(https://[^"'']*?(qdrant|weaviate|pinecone|chroma)|ssl\s*=\s*True|tls\s*=\s*True|grpcs://)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'TLS endpoints (PASS)' + - id: P13.3 + control: DSGAI13 + name: insecure-binding + classification: value_bearing + signal: warn + confidence: low + pcre: '(host[^\n]{0,15}0\.0\.0\.0|ALLOW_RESET\s*=\s*True|chroma[^\n]{0,30}http://localhost|qdrant[^\n]{0,30}http://localhost)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Insecure binding (WARN)' + - id: P13.4 + control: DSGAI13 + name: hardcoded-vector-token + classification: value_bearing + signal: fail + confidence: high + pcre: '(QDRANT_API_KEY|PINECONE_API_KEY|WEAVIATE_API_KEY)\s*=\s*["''][A-Za-z0-9_\-]{16,}' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Hardcoded vector token (FAIL)' + - id: P14.1 + control: DSGAI14 + name: prompt-logging-disabled + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(log_prompts\s*=\s*False|capture_content\s*=\s*False|OTEL_LOG_PROMPTS[^\n]{0,10}false|log_completions\s*=\s*False)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Prompt logging disabled (PASS)' + - id: P14.2 + control: DSGAI14 + name: prompt-logging-enabled + classification: value_bearing + signal: warn + confidence: low + pcre: '(log_prompts\s*=\s*True|capture_content\s*=\s*True|log_full_response\s*=\s*True)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Prompt logging enabled (WARN)' + - id: P14.3 + control: DSGAI14 + name: pii-redaction-in-log + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(redact_pii|mask_sensitive|sanitize_log|scrub[^\n]{0,10}log|log[^\n]{0,10}redact)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'PII redaction in log (PASS)' + - id: P14.4 + control: DSGAI14 + name: raw-response-print + classification: value_bearing + signal: warn + confidence: low + pcre: '(print\((response|completion)|console\.log\((response|completion)|logger\.(debug|info)\([^)]*(response|completion))' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Raw response print (WARN)' + notes: 'May contain inline PII in the format string — treated as VALUE-BEARING.' + - id: P14.5 + control: DSGAI14 + name: langsmith-langfuse-config + classification: value_bearing + signal: info + confidence: low + pcre: '(LANGSMITH_API_KEY|LANGFUSE_PUBLIC_KEY|langfuse[^\n]{0,20}init|langsmith[^\n]{0,20}trace)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.yaml', '*.env*'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'LangSmith / Langfuse config' + - id: P15.1 + control: DSGAI15 + name: secret-in-system-prompt + classification: value_bearing + signal: fail + confidence: high + pcre: '(system_prompt|system_message)\s*=\s*["''][^"'']*(api_key|password|secret|token|sk-)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Secret in system prompt (FAIL)' + - id: P15.2 + control: DSGAI15 + name: context-size-limit + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(max_context_length\s*=|context_window_limit\s*=|max_context_tokens\s*=|truncate_context)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Context size limit (PASS)' + - id: P15.3 + control: DSGAI15 + name: prompt-from-config + classification: value_bearing + signal: pass_signal + confidence: medium + pcre: '(system_prompt|system_message)[^\n]{0,40}(config|vault|os\.environ|getenv|secret_manager)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Prompt from config (PASS)' + - id: P15.4 + control: DSGAI15 + name: over-fetch-aggregation + classification: value_bearing + signal: warn + confidence: low + pcre: '(customer_360|user_profile_all|fetch_all_user_data|get_full_profile|select\s*\*\s*from\s+users)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Over-fetch aggregation (WARN)' + - id: P16.1 + control: DSGAI16 + name: ai-ignore-file-present + classification: structural + signal: pass_signal + confidence: medium + pcre: '(\.(copilot|ai|cursor|continue|codeium)ignore)' + file_globs: ['.copilotignore', '.aiignore', '.cursorignore', '.continueignore', '.codeiumignore', '.vscode/settings.json', 'cursor.json', 'continue.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'AI-ignore file present (PASS)' + notes: 'Filename-existence check (not content grep). Absence in a repo with .env or secrets/ = WARN.' + - id: P16.2 + control: DSGAI16 + name: sensitive-paths-excluded + classification: structural + signal: info + confidence: low + pcre: '(\.env|secrets|credentials|\.aws|\.ssh|private_key)' + file_globs: ['.copilotignore', '.aiignore', '.cursorignore', '.continueignore', '.codeiumignore', '.vscode/settings.json', 'cursor.json', 'continue.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Sensitive paths excluded' + notes: 'Matched inside any AI-ignore file.' + - id: P16.3 + control: DSGAI16 + name: telemetry-off + classification: structural + signal: pass_signal + confidence: medium + pcre: '(telemetry[^\n]{0,10}(off|false|disabled)|share_data\s*[:=]\s*false)' + file_globs: ['.copilotignore', '.aiignore', '.cursorignore', '.continueignore', '.codeiumignore', '.vscode/settings.json', 'cursor.json', 'continue.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Telemetry off (PASS)' + - id: P16.4 + control: DSGAI16 + name: context-scope-limits + classification: structural + signal: info + confidence: low + pcre: '(contextWindow|ignorePaths|excludeFiles|maxContextLines)' + file_globs: ['.copilotignore', '.aiignore', '.cursorignore', '.continueignore', '.codeiumignore', '.vscode/settings.json', 'cursor.json', 'continue.json'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Context scope limits' + - id: P17.1 + control: DSGAI17 + name: circuit-breaker + classification: structural + signal: pass_signal + confidence: medium + pcre: '(CircuitBreaker|@circuit|tenacity|resilience4j|pybreaker)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Circuit breaker (PASS)' + notes: 'LLM-calling module with none of P17.1-P17.5 = WARN.' + - id: P17.2 + control: DSGAI17 + name: retry-with-backoff + classification: structural + signal: pass_signal + confidence: medium + pcre: '(retry\s*\(|@retry|exponential_backoff|max_retries\s*=|backoff_factor\s*=|retry_with_backoff)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Retry with backoff (PASS)' + - id: P17.3 + control: DSGAI17 + name: timeout-on-llm-call + classification: structural + signal: pass_signal + confidence: medium + pcre: '(timeout\s*=\s*\d|request_timeout\s*=|httpx\.[^\n]{0,30}timeout)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Timeout on LLM call (PASS)' + - id: P17.4 + control: DSGAI17 + name: rate-limit-incoming + classification: structural + signal: pass_signal + confidence: medium + pcre: '(rate_limit|@throttle|RateLimiter|slowapi|flask_limiter|express-rate-limit)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Rate limit incoming (PASS)' + - id: P17.5 + control: DSGAI17 + name: fallback-response + classification: structural + signal: pass_signal + confidence: medium + pcre: '(fallback_response|default_response|graceful_degradation|on_failure_return)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Fallback response (PASS)' + - id: P17.6 + control: DSGAI17 + name: unbounded-retry + classification: structural + signal: fail + confidence: medium + pcre: 'while\s+True[^}]{0,200}(generate|complete|chat)|retry\s*\(\s*\)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Unbounded retry (FAIL)' + - id: P18.1 + control: DSGAI18 + name: output-guardrails + classification: structural + signal: pass_signal + confidence: medium + pcre: '(guardrails|nemo[._-]guardrails|llm_guard|output_guard|filter_output|sanitize_response|moderation)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Output guardrails (PASS)' + - id: P18.2 + control: DSGAI18 + name: pii-detection-on-output + classification: structural + signal: info + confidence: low + pcre: '(detect_pii[^\n]{0,20}output|output[^\n]{0,20}detect_pii|presidio[^\n]{0,20}output|scan_output)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'PII detection on output' + - id: P18.3 + control: DSGAI18 + name: logprobs-exposed + classification: structural + signal: warn + confidence: low + pcre: '(logprobs\s*=\s*True|return_logprobs\s*=\s*True|include_logprobs\s*=\s*True|top_logprobs\s*=\s*\d)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Logprobs exposed (WARN)' + - id: P18.4 + control: DSGAI18 + name: llm-call-sites + classification: structural + signal: count + confidence: medium + pcre: '(\.chat\.completions\.create|client\.messages\.create|openai\.ChatCompletion|anthropic\.messages|generate_content|chat\.complete)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'LLM call sites (count)' + requires_nearby: {rule: P18.5, lines: 10} + - id: P18.5 + control: DSGAI18 + name: max-tokens-set + classification: structural + signal: pass_signal + confidence: medium + pcre: 'max_tokens\s*=\s*\d+' + file_globs: ['*.py', '*.ts', '*.java', '*.kt', '*.go'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'max_tokens set (PASS)' + - id: P19.1 + control: DSGAI19 + name: anonymization-before-export + classification: structural + signal: info + confidence: low + pcre: '(anonymize_for_labeling|pseudonymize|redact_before_export|mask_for_labeling)' + file_globs: ['*.py', '*.ipynb', '*.ts', '*.yaml'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Anonymization before export' + gated_on: labeling + - id: P19.2 + control: DSGAI19 + name: labeling-sdk-auth-from-vault + classification: structural + signal: info + confidence: low + pcre: '(label_studio[^\n]{0,30}vault|labelbox[^\n]{0,30}secret|scale_api_key[^\n]{0,30}(vault|env))' + file_globs: ['*.py', '*.ipynb', '*.ts', '*.yaml'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Labeling SDK auth from vault' + gated_on: labeling + - id: P19.3 + control: DSGAI19 + name: labeling-access-audit + classification: structural + signal: info + confidence: low + pcre: '(label[._-]audit|labeling_access_log|annotator_audit)' + file_globs: ['*.py', '*.ipynb', '*.ts', '*.yaml'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Labeling access audit' + gated_on: labeling + - id: P19.4 + control: DSGAI19 + name: re-identification-post-label + classification: structural + signal: info + confidence: low + pcre: '(reid_check_post_label|labeling_reidentification|post_label_validation)' + file_globs: ['*.py', '*.ipynb', '*.ts', '*.yaml'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Re-identification post-label' + gated_on: labeling + - id: P20.1 + control: DSGAI20 + name: api-auth-required + classification: structural + 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'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'API auth required (PASS)' + - id: P20.2 + control: DSGAI20 + name: rate-limit + classification: structural + 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'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Rate limit (PASS)' + - id: P20.3 + control: DSGAI20 + name: input-length-validation + classification: structural + 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'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Input length validation (PASS)' + - id: P20.4 + control: DSGAI20 + name: prompt-injection-detect + classification: structural + 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'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Prompt injection detect (PASS)' + - id: P20.5 + control: DSGAI20 + name: inference-endpoint + classification: structural + 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'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Inference endpoint (count)' + requires_nearby: {rules: ['P20.1', 'P20.2'], lines: 15} + - id: P21.1 + control: DSGAI21 + name: read-only-retrieval + classification: structural + signal: pass_signal + confidence: medium + pcre: '(read_only\s*=\s*True|readonly\s*=\s*True|HttpMethod\.GET|http_method[^\n]{0,10}get)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Read-only retrieval (PASS)' + - id: P21.2 + control: DSGAI21 + name: kb-write-from-agent + classification: structural + signal: warn + confidence: low + pcre: '(chromadb|qdrant|pinecone|weaviate|knowledge_base|kb_client)[^\n]{0,40}\.(upsert|insert|update|delete|add_documents|write)\(' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'KB write from agent (WARN)' + notes: 'P21.2 in an agent module without P21.3 = WARN.' + - id: P21.3 + control: DSGAI21 + name: write-content-validation + classification: structural + signal: info + confidence: low + pcre: '(validate_content|sanitize[._-]write|verify[._-]knowledge|content[._-]check[._-]write)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'Write content validation' + - id: P21.4 + control: DSGAI21 + name: kb-versioning + classification: structural + signal: pass_signal + confidence: medium + pcre: '(versioned\s*=\s*True|audit_writes|version_id|kb_audit|knowledge[._-]audit)' + file_globs: ['*.py', '*.ts', '*.java', '*.kt'] + exclude_globs: [] + framework: 'dsgai-2026-v1.0' + description: 'KB versioning (PASS)' diff --git a/dsgai_scanner_tool/rules/rules.schema.json b/dsgai_scanner_tool/rules/rules.schema.json new file mode 100644 index 0000000..5b392e8 --- /dev/null +++ b/dsgai_scanner_tool/rules/rules.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/dsgai_scanner_tool/rules/rules.schema.json", + "title": "DSGAI scanner ruleset", + "description": "Schema for rules/dsgai-rules.yaml. Note: JSON Schema cannot verify that a `pcre` string actually compiles under rg --pcre2 — the self-test CI (PR-06) does that.", + "type": "object", + "required": ["ruleset_version", "framework", "rules"], + "additionalProperties": false, + "properties": { + "ruleset_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "framework": { "type": "string", "pattern": "^dsgai-[0-9]{4}-v[0-9]+\\.[0-9]+$" }, + "rules": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rule" } + } + }, + "$defs": { + "ruleId": { "type": "string", "pattern": "^P[0-9]{2}\\.[0-9]+$" }, + "rule": { + "type": "object", + "required": [ + "id", "control", "name", "classification", "signal", + "confidence", "pcre", "file_globs", "framework", "description" + ], + "additionalProperties": false, + "properties": { + "id": { "$ref": "#/$defs/ruleId" }, + "control": { "type": "string", "pattern": "^DSGAI[0-9]{2}$" }, + "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "classification": { "enum": ["structural", "value_bearing"] }, + "signal": { "enum": ["fail", "warn", "pass_signal", "count", "info"] }, + "confidence": { "enum": ["high", "medium", "low"] }, + "pcre": { "type": "string", "minLength": 1 }, + "file_globs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "exclude_globs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "framework": { "type": "string", "pattern": "^dsgai-[0-9]{4}-v[0-9]+\\.[0-9]+$" }, + "description": { "type": "string", "minLength": 1 }, + "remediation": { "type": "string" }, + "references": { "type": "array", "items": { "type": "string" } }, + "subtract": { + "type": "array", + "items": { "$ref": "#/$defs/ruleId" } + }, + "requires_nearby": { + "type": "object", + "additionalProperties": false, + "properties": { + "rule": { "$ref": "#/$defs/ruleId" }, + "rules": { "type": "array", "items": { "$ref": "#/$defs/ruleId" } }, + "lines": { "type": "integer", "minimum": 0 }, + "scope": { "enum": ["module", "file"] }, + "absent": { "type": "boolean" } + }, + "anyOf": [ + { "required": ["rule"] }, + { "required": ["rules"] } + ] + }, + "gated_on": { "enum": ["multimodal", "synthetic_data", "labeling"] }, + "notes": { "type": "string" } + } + } + } +}