Skip to content

Commit ba651aa

Browse files
committed
feat(scanner): C#/Rust/Ruby ecosystems + Semgrep rule-pack export (PR-15)
- Detection signals for C# (Semantic Kernel / Azure.AI.OpenAI), Rust (async-openai), Ruby (ruby-openai). - CVE manifest parsing for NuGet (*.csproj), crates.io (Cargo.lock), RubyGems (Gemfile.lock) via OSV — supply-chain coverage for the three ecosystems. - Credential coverage extended to *.cs/*.rs/*.rb/*.csproj on 13 DSGAI02/DSGAI13 rules, with C#/Rust/Ruby fixture cases (P02.9 catches raw tokens); answer sheet 29 -> 32. - build/export_semgrep.py -> dist/dsgai.semgrep.yaml: the 85 STRUCTURAL rules as a Semgrep pack (value-bearing excluded by design — their matches must never surface). Generated from the rules YAML; drift is a CI failure (--check). Makes incumbent toolchains carriers of the DSGAI framework. - test_runner: the fixture scan now runs --no-cve so CI is deterministic (no network); added semgrep-sync and multi-ecosystem-parse tests. Verified: regen --check (32 pins); pytest 21 passed; semgrep pack valid YAML (85 rules, all pattern-regex + dsgai metadata); yamllint clean under CI config.
1 parent f275cf2 commit ba651aa

12 files changed

Lines changed: 1351 additions & 38 deletions

File tree

dsgai_scanner_tool/CHANGES_v0.3.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ dates are ISO-8601. The previous line is recorded in [`CHANGES_v0.2.md`](CHANGES
99
## [Unreleased]
1010

1111
### Added
12+
- **Ecosystem expansion + rule-pack export** (PR-15).
13+
- **C# / Rust / Ruby**: detection signals (Semantic Kernel/Azure.AI.OpenAI, async-openai,
14+
ruby-openai); CVE manifest parsing for **NuGet** (`*.csproj`), **crates.io**
15+
(`Cargo.lock`), **RubyGems** (`Gemfile.lock`) via OSV; credential coverage extended to
16+
`*.cs`/`*.rs`/`*.rb`/`*.csproj` (13 DSGAI02/13 rules), with C#/Rust/Ruby fixture cases.
17+
- **`build/export_semgrep.py``dist/dsgai.semgrep.yaml`**: exports the 85 STRUCTURAL
18+
rules as a Semgrep pack (value-bearing excluded by design) so incumbent toolchains
19+
carry the DSGAI framework. Generated from the rules YAML; drift is a CI failure.
1220
- **Templated report, single-sourced prompt variant, static ATLAS map** (PR-14).
1321
- `cli/dsgai_report.py` + `templates/report.css`: the HTML report is now rendered
1422
**by code** from the checkpoint (deterministic, testable), with a golden structural
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
"""Export the STRUCTURAL DSGAI rules as a Semgrep pack.
3+
4+
Strategic point: this makes incumbent toolchains carriers of the DSGAI framework
5+
— distribution, not competition. Generated from rules/dsgai-rules.yaml so it
6+
can't drift (CI runs `--check`). Value-bearing rules are intentionally excluded
7+
(their whole point is that the match content must never be surfaced, which a
8+
generic Semgrep pack cannot guarantee).
9+
10+
python build/export_semgrep.py # write dist/dsgai.semgrep.yaml
11+
python build/export_semgrep.py --check # CI: fail if the pack is stale
12+
"""
13+
import sys
14+
from pathlib import Path
15+
16+
ROOT = Path(__file__).resolve().parent.parent
17+
RULES = ROOT / "rules" / "dsgai-rules.yaml"
18+
OUT = ROOT / "dist" / "dsgai.semgrep.yaml"
19+
SEVERITY = {"fail": "ERROR", "warn": "WARNING"}
20+
21+
22+
def yq(s):
23+
return "'" + s.replace("'", "''") + "'"
24+
25+
26+
def render():
27+
import yaml
28+
data = yaml.safe_load(RULES.read_text(encoding="utf-8"))
29+
out = [
30+
"# DSGAI STRUCTURAL rules as a Semgrep pack.",
31+
"# GENERATED from rules/dsgai-rules.yaml by build/export_semgrep.py — do not edit.",
32+
"# Value-bearing rules are excluded by design (their matches must never surface).",
33+
"rules:",
34+
]
35+
for r in data["rules"]:
36+
if r["classification"] != "structural":
37+
continue
38+
sev = SEVERITY.get(r["signal"], "INFO")
39+
includes = ", ".join(yq(g) for g in r["file_globs"])
40+
out += [
41+
f" - id: dsgai-{r['id']}",
42+
f" languages: [generic]",
43+
f" severity: {sev}",
44+
f" message: {yq(r['control'] + ' ' + r['id'] + ': ' + r['description'])}",
45+
f" patterns:",
46+
f" - pattern-regex: {yq(r['pcre'])}",
47+
f" paths:",
48+
f" include: [{includes}]",
49+
f" metadata:",
50+
f" dsgai_control: {r['control']}",
51+
f" dsgai_rule: {r['id']}",
52+
f" confidence: {r['confidence']}",
53+
f" framework: {yq(r['framework'])}",
54+
]
55+
return "\n".join(out) + "\n"
56+
57+
58+
def main(argv):
59+
rendered = render()
60+
if "--check" in argv:
61+
current = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
62+
if current != rendered:
63+
sys.stderr.write("dist/dsgai.semgrep.yaml is out of date. Run: "
64+
"python build/export_semgrep.py\n")
65+
return 1
66+
print("dist/dsgai.semgrep.yaml is up to date.")
67+
return 0
68+
OUT.parent.mkdir(exist_ok=True)
69+
OUT.write_text(rendered, encoding="utf-8", newline="\n")
70+
n = rendered.count(" - id: dsgai-")
71+
print(f"wrote {OUT} ({n} structural rules)")
72+
return 0
73+
74+
75+
if __name__ == "__main__":
76+
sys.exit(main(sys.argv[1:]))

dsgai_scanner_tool/cli/dsgai_cve.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@
3333
}
3434

3535
_REQ_RE = re.compile(r'^\s*([A-Za-z0-9_.\-]+)\s*==\s*([A-Za-z0-9_.\-]+)')
36+
_CSPROJ_RE = re.compile(r'<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"')
37+
_GEMLOCK_RE = re.compile(r'^\s{4}([A-Za-z0-9_.\-]+) \(([0-9][A-Za-z0-9_.\-]*)\)\s*$')
38+
39+
40+
def _parse_cargo_lock(ap):
41+
deps, name = [], None
42+
for line in open(ap, encoding="utf-8", errors="replace"):
43+
s = line.strip()
44+
if s.startswith("name = "):
45+
name = s.split('"')[1] if '"' in s else None
46+
elif s.startswith("version = ") and name:
47+
deps.append(("crates.io", name, s.split('"')[1]))
48+
name = None
49+
return deps
3650

3751

3852
def _cache_path(eco, pkg, ver):
@@ -66,23 +80,36 @@ def parse_dependencies(discovered):
6680
queried (OSV needs a concrete version).
6781
"""
6882
deps, seen = [], set()
83+
84+
def add(eco, pkg, ver):
85+
key = (eco, pkg.lower(), ver)
86+
if key not in seen:
87+
seen.add(key)
88+
deps.append({"ecosystem": eco, "package": pkg, "version": ver})
89+
6990
for ap, rel in discovered:
7091
base = os.path.basename(rel)
71-
eco = ECOSYSTEMS.get(base)
72-
if eco == "PyPI" and base.startswith("requirements"):
73-
try:
92+
try:
93+
if base.startswith("requirements") and base.endswith(".txt"):
7494
for line in open(ap, encoding="utf-8", errors="replace"):
7595
if line.lstrip().startswith("#"):
7696
continue
7797
m = _REQ_RE.match(line)
7898
if m:
79-
key = (eco, m.group(1).lower(), m.group(2))
80-
if key not in seen:
81-
seen.add(key)
82-
deps.append({"ecosystem": eco, "package": m.group(1),
83-
"version": m.group(2)})
84-
except OSError:
85-
continue
99+
add("PyPI", m.group(1), m.group(2))
100+
elif base == "Cargo.lock":
101+
for eco, pkg, ver in _parse_cargo_lock(ap):
102+
add(eco, pkg, ver)
103+
elif base == "Gemfile.lock":
104+
for line in open(ap, encoding="utf-8", errors="replace"):
105+
m = _GEMLOCK_RE.match(line.rstrip("\n"))
106+
if m:
107+
add("RubyGems", m.group(1), m.group(2))
108+
elif base.endswith(".csproj"):
109+
for m in _CSPROJ_RE.finditer(open(ap, encoding="utf-8", errors="replace").read()):
110+
add("NuGet", m.group(1), m.group(2))
111+
except OSError:
112+
continue
86113
return deps
87114

88115

dsgai_scanner_tool/cli/dsgai_scan.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
"llm": [r"openai", r"anthropic", r"langchain", r"llama_index", r"llama-index",
6363
r"cohere", r"mistralai", r"litellm", r"api\.openai\.com",
6464
r"api\.anthropic\.com", r"generativelanguage\.googleapis\.com",
65-
r"Microsoft\.SemanticKernel", r"Azure\.AI\.OpenAI"],
65+
r"Microsoft\.SemanticKernel", r"Azure\.AI\.OpenAI", # C# / NuGet
66+
r"async-openai", r"async_openai", # Rust / crates.io
67+
r"ruby-openai", r"ruby_openai", r"anthropic-rb"], # Ruby / RubyGems
6668
"multimodal": [r"vision", r"image_url", r"ocr", r"whisper", r"audio",
6769
r"detect_pii_in_image"],
6870
"synthetic_data": [r"\bSDV\b", r"gretel", r"synthetic_data_vault", r"smartnoise"],

0 commit comments

Comments
 (0)