Skip to content

Commit 089d42b

Browse files
authored
ci(scanner): repo hygiene — non-goals, CODEOWNERS, lint CI, image compression (PR-02) (#25)
- README: add a Non-goals section (the maintainer's scope shield). - .github/CODEOWNERS: route dsgai_scanner_tool/ to @emmanuelgjr. - .github/workflows/scanner-lint.yml: path-filtered lint (shellcheck -S warning, yamllint, deterministic internal markdown-link check). All three pass locally. - scripts/check_md_links.py: dependency-free internal-link/anchor checker (no flaky external HTTP in a required gate). - .gitattributes: force LF on *.sh/*.py/*.yml/*.yaml so Windows autocrlf checkouts can't ship CRLF that breaks shellcheck/bash on Linux CI. - DSGAI-samplereport.png: 5.0 MB -> 0.35 MB (14x) interim compression; full regeneration from the fixture app lands in PR-09. dependabot already covers the github-actions ecosystem; pip is added in PR-05 with the CLI manifest. shellcheck is clean at warning level; the mapfile/grep -zE portability issues (lines 18/20) are bash-3.2/BSD concerns shellcheck can't see without shell context — deferred to PR-10 as planned. Annotated tag scanner-v0.2.0 created locally; push held for maintainer approval per plan.
1 parent b8ff971 commit 089d42b

7 files changed

Lines changed: 174 additions & 1 deletion

File tree

‎.github/CODEOWNERS‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Code owners for the GenAI-Data-Security-Initiative monorepo.
2+
# Docs: https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
3+
#
4+
# Owners are requested for review automatically when matching files change.
5+
6+
# DSGAI scanner tool subproject
7+
/dsgai_scanner_tool/ @emmanuelgjr

‎.github/workflows/scanner-lint.yml‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: scanner-lint
2+
3+
# Path-filtered lint for the DSGAI scanner subproject only, to keep monorepo
4+
# CI noise down. This is the pattern all later scanner CI follows.
5+
on:
6+
push:
7+
branches: [main]
8+
paths:
9+
- 'dsgai_scanner_tool/**'
10+
- '.github/workflows/scanner-lint.yml'
11+
pull_request:
12+
paths:
13+
- 'dsgai_scanner_tool/**'
14+
- '.github/workflows/scanner-lint.yml'
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
lint:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25+
26+
- name: shellcheck integrations shell scripts
27+
run: |
28+
sudo apt-get update -qq && sudo apt-get install -y shellcheck
29+
# -S warning: report warnings and errors. Known dsgai-secret-scan.sh
30+
# issues are suppressed inline with TODO(PR-10) markers until PR-10.
31+
find dsgai_scanner_tool/integrations -name '*.sh' -print0 \
32+
| xargs -0 -r shellcheck -S warning
33+
34+
- name: yamllint scanner YAML
35+
run: |
36+
python -m pip install --quiet yamllint
37+
yamllint -d "{extends: relaxed, rules: {line-length: disable, document-start: disable, new-lines: disable, truthy: {check-keys: false}}}" \
38+
dsgai_scanner_tool/
39+
40+
- name: Markdown link check (scanner docs — relative links + anchors)
41+
run: |
42+
# Deterministic internal-link check: verifies relative file links and
43+
# in-repo heading anchors resolve. External URLs are intentionally not
44+
# fetched (flaky in CI); that is a separate concern.
45+
python dsgai_scanner_tool/scripts/check_md_links.py dsgai_scanner_tool

‎dsgai_scanner_tool/.gitattributes‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Force LF for scripts so Windows checkouts (core.autocrlf=true) can't ship
2+
# CRLF that breaks shellcheck / bash on Linux CI runners.
3+
*.sh text eol=lf
4+
*.py text eol=lf
5+
*.yml text eol=lf
6+
*.yaml text eol=lf

‎dsgai_scanner_tool/CHANGES_v0.3.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@ dates are ISO-8601. The previous line is recorded in [`CHANGES_v0.2.md`](CHANGES
1212
- Contributor infrastructure: `[scanner]` GitHub issue-form templates (false-positive,
1313
false-negative, new-rule, bug), scanner `CONTRIBUTING.md`, public `ROADMAP.md`, and
1414
this changelog scaffold. (PR-01)
15+
- Repo hygiene: **Non-goals** section in the README, root `CODEOWNERS` for
16+
`dsgai_scanner_tool/`, path-filtered `scanner-lint.yml` CI (shellcheck + yamllint +
17+
internal-link check), a deterministic markdown internal-link checker
18+
(`scripts/check_md_links.py`), and a `.gitattributes` forcing LF on scripts/YAML so
19+
Windows checkouts can't ship CRLF that breaks Linux CI. (PR-02)
1520

1621
### Changed
17-
- _nothing yet_
22+
- `DSGAI-samplereport.png` compressed from ~5.0 MB to ~0.35 MB (14×) as an interim fix;
23+
full regeneration from the fixture app lands in PR-09. (PR-02)
1824

1925
### Fixed
2026
- _nothing yet_
-4.54 MB
Loading

‎dsgai_scanner_tool/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,24 @@ When proposing new scan patterns:
408408
2. Validate the PCRE pattern with `rg --pcre2 'pattern' .` against a real repo
409409
3. For VALUE-BEARING patterns, prove the value never escapes by inspecting `DSGAI-scan.json` after a test run
410410

411+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full contributor guide and the
412+
[`ROADMAP.md`](ROADMAP.md) for what's planned.
413+
414+
## Non-goals
415+
416+
To keep the scanner maintainable and trustworthy, some things are deliberately out of scope:
417+
418+
- **We will not reimplement general-purpose secret scanning.** We ship a gitleaks rule
419+
pack instead (see `integrations/gitleaks/`) and lean on battle-tested tooling for
420+
entropy-based detection.
421+
- **We will not become a general-purpose SAST tool.** Scope is the 21 DSGAI controls and
422+
the GenAI-specific patterns behind them — not every code smell in a repo.
423+
- **We will not add rules without fixture test cases.** A rule with no positive *and*
424+
negative test has no defined precision, so it doesn't merge.
425+
- **We will not accept changes that weaken the redaction guarantees.** Value-bearing
426+
matches never enter a report, checkpoint, or persisted tool call — that property is
427+
non-negotiable.
428+
411429
---
412430

413431
## License
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
"""Deterministic internal-link checker for the DSGAI scanner docs.
3+
4+
Verifies that relative markdown links point at files that exist, and that
5+
in-file `#anchor` fragments match a heading in the target document. External
6+
(http/https/mailto) links are intentionally not fetched — that is flaky in CI
7+
and a separate concern. Exit 1 if any internal link is broken.
8+
9+
Usage: python check_md_links.py <dir-or-file> [more...]
10+
"""
11+
import re, sys, unicodedata
12+
from pathlib import Path
13+
14+
LINK_RE = re.compile(r'(?<!\!)\[[^\]]*\]\(([^)]+)\)')
15+
HEADING_RE = re.compile(r'^(#{1,6})\s+(.*?)\s*#*\s*$')
16+
17+
18+
def slug(text: str) -> str:
19+
"""GitHub-style heading -> anchor slug."""
20+
text = unicodedata.normalize('NFKD', text)
21+
# strip markdown inline formatting and links
22+
text = re.sub(r'`([^`]*)`', r'\1', text)
23+
text = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', text)
24+
text = re.sub(r'[*_~]', '', text)
25+
text = text.lower()
26+
text = re.sub(r'[^\w\s-]', '', text)
27+
text = text.strip().replace(' ', '-')
28+
return text
29+
30+
31+
def anchors_of(path: Path) -> set:
32+
out = set()
33+
if not path.exists():
34+
return out
35+
for line in path.read_text(encoding='utf-8', errors='replace').splitlines():
36+
m = HEADING_RE.match(line)
37+
if m:
38+
out.add(slug(m.group(2)))
39+
return out
40+
41+
42+
def collect_md(targets):
43+
files = []
44+
for t in targets:
45+
p = Path(t)
46+
if p.is_dir():
47+
files += sorted(p.rglob('*.md'))
48+
elif p.suffix == '.md':
49+
files.append(p)
50+
return files
51+
52+
53+
def main():
54+
targets = sys.argv[1:] or ['.']
55+
files = collect_md(targets)
56+
anchor_cache = {}
57+
broken = []
58+
for f in files:
59+
text = f.read_text(encoding='utf-8', errors='replace')
60+
for m in LINK_RE.finditer(text):
61+
target = m.group(1).strip()
62+
if target.startswith('<') and target.endswith('>'):
63+
target = target[1:-1]
64+
# skip external and pure-anchor-to-external schemes
65+
if re.match(r'^[a-z]+://', target) or target.startswith('mailto:'):
66+
continue
67+
path_part, _, frag = target.partition('#')
68+
if path_part == '':
69+
# same-file anchor
70+
dest = f
71+
else:
72+
dest = (f.parent / path_part).resolve()
73+
if not dest.exists():
74+
broken.append(f"{f}: missing file -> {target}")
75+
continue
76+
if frag:
77+
if dest not in anchor_cache:
78+
anchor_cache[dest] = anchors_of(dest)
79+
if slug(frag) not in anchor_cache[dest]:
80+
broken.append(f"{f}: missing anchor -> {target}")
81+
if broken:
82+
print("Broken internal links:")
83+
for b in broken:
84+
print(" " + b)
85+
return 1
86+
print(f"OK: {len(files)} markdown files, all internal links resolve.")
87+
return 0
88+
89+
90+
if __name__ == '__main__':
91+
sys.exit(main())

0 commit comments

Comments
 (0)