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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dsgai_scanner_tool/CHANGES_v0.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ dates are ISO-8601. The previous line is recorded in [`CHANGES_v0.2.md`](CHANGES
## [Unreleased]

### Added
- **Ecosystem expansion + rule-pack export** (PR-15).
- **C# / Rust / Ruby**: detection signals (Semantic Kernel/Azure.AI.OpenAI, async-openai,
ruby-openai); CVE manifest parsing for **NuGet** (`*.csproj`), **crates.io**
(`Cargo.lock`), **RubyGems** (`Gemfile.lock`) via OSV; credential coverage extended to
`*.cs`/`*.rs`/`*.rb`/`*.csproj` (13 DSGAI02/13 rules), with C#/Rust/Ruby fixture cases.
- **`build/export_semgrep.py` → `dist/dsgai.semgrep.yaml`**: exports the 85 STRUCTURAL
rules as a Semgrep pack (value-bearing excluded by design) so incumbent toolchains
carry the DSGAI framework. Generated from the rules YAML; drift is a CI failure.
- **Templated report, single-sourced prompt variant, static ATLAS map** (PR-14).
- `cli/dsgai_report.py` + `templates/report.css`: the HTML report is now rendered
**by code** from the checkpoint (deterministic, testable), with a golden structural
Expand Down
76 changes: 76 additions & 0 deletions dsgai_scanner_tool/build/export_semgrep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Export the STRUCTURAL DSGAI rules as a Semgrep pack.

Strategic point: this makes incumbent toolchains carriers of the DSGAI framework
— distribution, not competition. Generated from rules/dsgai-rules.yaml so it
can't drift (CI runs `--check`). Value-bearing rules are intentionally excluded
(their whole point is that the match content must never be surfaced, which a
generic Semgrep pack cannot guarantee).

python build/export_semgrep.py # write dist/dsgai.semgrep.yaml
python build/export_semgrep.py --check # CI: fail if the pack is stale
"""
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
RULES = ROOT / "rules" / "dsgai-rules.yaml"
OUT = ROOT / "dist" / "dsgai.semgrep.yaml"
SEVERITY = {"fail": "ERROR", "warn": "WARNING"}


def yq(s):
return "'" + s.replace("'", "''") + "'"


def render():
import yaml
data = yaml.safe_load(RULES.read_text(encoding="utf-8"))
out = [
"# DSGAI STRUCTURAL rules as a Semgrep pack.",
"# GENERATED from rules/dsgai-rules.yaml by build/export_semgrep.py — do not edit.",
"# Value-bearing rules are excluded by design (their matches must never surface).",
"rules:",
]
for r in data["rules"]:
if r["classification"] != "structural":
continue
sev = SEVERITY.get(r["signal"], "INFO")
includes = ", ".join(yq(g) for g in r["file_globs"])
out += [
f" - id: dsgai-{r['id']}",
f" languages: [generic]",
f" severity: {sev}",
f" message: {yq(r['control'] + ' ' + r['id'] + ': ' + r['description'])}",
f" patterns:",
f" - pattern-regex: {yq(r['pcre'])}",
f" paths:",
f" include: [{includes}]",
f" metadata:",
f" dsgai_control: {r['control']}",
f" dsgai_rule: {r['id']}",
f" confidence: {r['confidence']}",
f" framework: {yq(r['framework'])}",
]
return "\n".join(out) + "\n"


def main(argv):
rendered = render()
if "--check" in argv:
current = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
if current != rendered:
sys.stderr.write("dist/dsgai.semgrep.yaml is out of date. Run: "
"python build/export_semgrep.py\n")
return 1
print("dist/dsgai.semgrep.yaml is up to date.")
return 0
OUT.parent.mkdir(exist_ok=True)
OUT.write_text(rendered, encoding="utf-8", newline="\n")
n = rendered.count(" - id: dsgai-")
print(f"wrote {OUT} ({n} structural rules)")
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
47 changes: 37 additions & 10 deletions dsgai_scanner_tool/cli/dsgai_cve.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@
}

_REQ_RE = re.compile(r'^\s*([A-Za-z0-9_.\-]+)\s*==\s*([A-Za-z0-9_.\-]+)')
_CSPROJ_RE = re.compile(r'<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"')
_GEMLOCK_RE = re.compile(r'^\s{4}([A-Za-z0-9_.\-]+) \(([0-9][A-Za-z0-9_.\-]*)\)\s*$')


def _parse_cargo_lock(ap):
deps, name = [], None
for line in open(ap, encoding="utf-8", errors="replace"):
s = line.strip()
if s.startswith("name = "):
name = s.split('"')[1] if '"' in s else None
elif s.startswith("version = ") and name:
deps.append(("crates.io", name, s.split('"')[1]))
name = None
return deps


def _cache_path(eco, pkg, ver):
Expand Down Expand Up @@ -66,23 +80,36 @@ def parse_dependencies(discovered):
queried (OSV needs a concrete version).
"""
deps, seen = [], set()

def add(eco, pkg, ver):
key = (eco, pkg.lower(), ver)
if key not in seen:
seen.add(key)
deps.append({"ecosystem": eco, "package": pkg, "version": ver})

for ap, rel in discovered:
base = os.path.basename(rel)
eco = ECOSYSTEMS.get(base)
if eco == "PyPI" and base.startswith("requirements"):
try:
try:
if base.startswith("requirements") and base.endswith(".txt"):
for line in open(ap, encoding="utf-8", errors="replace"):
if line.lstrip().startswith("#"):
continue
m = _REQ_RE.match(line)
if m:
key = (eco, m.group(1).lower(), m.group(2))
if key not in seen:
seen.add(key)
deps.append({"ecosystem": eco, "package": m.group(1),
"version": m.group(2)})
except OSError:
continue
add("PyPI", m.group(1), m.group(2))
elif base == "Cargo.lock":
for eco, pkg, ver in _parse_cargo_lock(ap):
add(eco, pkg, ver)
elif base == "Gemfile.lock":
for line in open(ap, encoding="utf-8", errors="replace"):
m = _GEMLOCK_RE.match(line.rstrip("\n"))
if m:
add("RubyGems", m.group(1), m.group(2))
elif base.endswith(".csproj"):
for m in _CSPROJ_RE.finditer(open(ap, encoding="utf-8", errors="replace").read()):
add("NuGet", m.group(1), m.group(2))
except OSError:
continue
return deps


Expand Down
4 changes: 3 additions & 1 deletion dsgai_scanner_tool/cli/dsgai_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"llm": [r"openai", r"anthropic", r"langchain", r"llama_index", r"llama-index",
r"cohere", r"mistralai", r"litellm", r"api\.openai\.com",
r"api\.anthropic\.com", r"generativelanguage\.googleapis\.com",
r"Microsoft\.SemanticKernel", r"Azure\.AI\.OpenAI"],
r"Microsoft\.SemanticKernel", r"Azure\.AI\.OpenAI", # C# / NuGet
r"async-openai", r"async_openai", # Rust / crates.io
r"ruby-openai", r"ruby_openai", r"anthropic-rb"], # Ruby / RubyGems
"multimodal": [r"vision", r"image_url", r"ocr", r"whisper", r"audio",
r"detect_pii_in_image"],
"synthetic_data": [r"\bSDV\b", r"gretel", r"synthetic_data_vault", r"smartnoise"],
Expand Down
Loading
Loading