@lhi/tdd-audit 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/badge.js CHANGED
@@ -8,6 +8,24 @@ const BADGE_MARKER = 'tdd-audit-badge';
8
8
 
9
9
  const NPM_URL = 'https://www.npmjs.com/package/@lhi/tdd-audit';
10
10
 
11
+ /**
12
+ * Validate that a URL uses http: or https:. Returns the URL if safe, NPM_URL otherwise.
13
+ * Prevents javascript:, data:, file:, and protocol-relative URLs from landing in
14
+ * badge links or SARIF output.
15
+ * @param {string} raw
16
+ * @returns {string}
17
+ */
18
+ function safeSiteUrl(raw) {
19
+ if (!raw || !raw.trim()) return NPM_URL;
20
+ try {
21
+ const parsed = new URL(raw.trim());
22
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return NPM_URL;
23
+ return raw.trim();
24
+ } catch {
25
+ return NPM_URL;
26
+ }
27
+ }
28
+
11
29
  /**
12
30
  * Build a shields.io badge markdown line reflecting actual scan results.
13
31
  *
@@ -48,11 +66,13 @@ function badgeLine(findings, siteUrl, label) {
48
66
  color = 'brightgreen';
49
67
  }
50
68
 
51
- const badgeLabel = (label && label.trim()) ? label.trim() : 'tdd-audit';
69
+ const rawLabel = (label && label.trim()) ? label.trim() : 'tdd-audit';
70
+ // Strip markdown link-breaking characters before embedding in alt text or URL
71
+ const badgeLabel = rawLabel.replace(/[[\]()]/g, '');
52
72
  // Encode the label for use in a shields.io URL (spaces → %20, hyphens → --)
53
73
  const encodedLabel = badgeLabel.replace(/ /g, '%20').replace(/-/g, '--');
54
74
  const badgeUrl = `https://img.shields.io/badge/${encodedLabel}-${message}-${color}`;
55
- const targetUrl = (siteUrl && siteUrl.trim()) ? siteUrl.trim() : NPM_URL;
75
+ const targetUrl = safeSiteUrl(siteUrl);
56
76
  // Embed the marker as a hidden HTML comment after the badge so injectBadge()
57
77
  // can locate and replace the line on subsequent runs.
58
78
  return `[![${badgeLabel}](${badgeUrl})](${targetUrl}) <!-- ${BADGE_MARKER} -->\n`;
package/lib/config.js CHANGED
@@ -23,6 +23,8 @@ const DEFAULTS = {
23
23
  org: null, // org name in reports, SECURITY.md, and pattern PRs
24
24
  project: null, // project name in reports and pattern contribution branch names
25
25
  badge_label: null, // badge label text; defaults to 'tdd-audit'
26
+ security_name: null, // name of the security contact (stamped into SECURITY.md, compliance reports, and webhook payloads)
27
+ security_email: null, // email of the security contact (used as the vulnerability reporting address in SECURITY.md)
26
28
 
27
29
  // Extensibility — both the CLI and the Claude Code skill honour these
28
30
  pattern_repos: [], // [{name, url, local_path, namespace}] — RAG-indexed at startup
@@ -30,6 +32,33 @@ const DEFAULTS = {
30
32
  extra_repos: [], // [{url, local_path}] — cloned/pulled for reference
31
33
  mcp_services: [], // [{name, cwd, command, args}] — started before first agent turn
32
34
  extra_domains: [], // [{name, prompt_file}] — custom audit domains
35
+
36
+ // Policy as code — org-level severity overrides
37
+ // e.g. { "CORS Wildcard": "CRITICAL", "Sensitive Log": "HIGH" }
38
+ severity_overrides: {},
39
+
40
+ // Notifications — fire on scan complete
41
+ webhook_url: null, // POST findings JSON to this URL on scan complete
42
+ slack_webhook: null, // Slack incoming webhook URL for findings summary
43
+ slack_channel: null, // override default channel for the Slack webhook
44
+
45
+ // Workflow integration
46
+ open_pr: false, // open a GitHub PR per finding instead of committing directly
47
+ github_token: null, // token for PR creation; falls back to GITHUB_TOKEN env var
48
+ github_repo: null, // 'owner/repo' for PR creation; auto-detected from git remote if null
49
+
50
+ // Scheduled / CI modes
51
+ schedule: null, // cron expression — used by external schedulers, not the CLI itself
52
+ pr_mode: false, // lightweight scan only (no agents, no RAG) — designed for CI PR gates
53
+ org_scan: null, // GitHub org name — scan all repos in the org
54
+
55
+ // Output additions
56
+ sbom: false, // generate a CycloneDX SBOM alongside the audit report
57
+ report: false, // generate a human-readable compliance report (PDF/markdown)
58
+ watch: false, // re-scan affected files on change (watch mode)
59
+
60
+ // Secret rotation — when a hardcoded key is found, offer to rotate via provider API
61
+ rotate_secrets: false, // prompt to rotate detected secrets via provider API
33
62
  };
34
63
 
35
64
  // Provider-specific defaults for `tdd-audit init --provider <name>`
@@ -129,7 +158,15 @@ function parseCliOverrides(args) {
129
158
  const baseUrl = get('--base-url'); if (baseUrl) overrides.baseUrl = baseUrl;
130
159
  const format = get('--format'); if (format) overrides.output = format;
131
160
  const srvKey = get('--api-key'); if (srvKey) overrides.serverApiKey = srvKey;
132
- if (args.includes('--json')) overrides.output = 'json';
161
+ const threshold = get('--threshold'); if (threshold) overrides.severityThreshold = threshold;
162
+ const org = get('--org'); if (org) overrides.org_scan = org;
163
+ if (args.includes('--json')) overrides.output = 'json';
164
+ if (args.includes('--pr')) overrides.pr_mode = true;
165
+ if (args.includes('--open-pr')) overrides.open_pr = true;
166
+ if (args.includes('--sbom')) overrides.sbom = true;
167
+ if (args.includes('--watch')) overrides.watch = true;
168
+ if (args.includes('--report')) overrides.report = true;
169
+ if (args.includes('--rotate-secrets')) overrides.rotate_secrets = true;
133
170
  return overrides;
134
171
  }
135
172
 
package/lib/github.js CHANGED
@@ -90,4 +90,4 @@ function parseRepo(repoStr) {
90
90
  return { owner, repo };
91
91
  }
92
92
 
93
- module.exports = { uploadSarif, postReviewComments, parseRepo };
93
+ module.exports = { uploadSarif, postReviewComments, parseRepo, ghFetch };
package/lib/reporter.js CHANGED
@@ -8,16 +8,17 @@ const { version } = require('../package.json');
8
8
  * Return findings as a structured JSON-serialisable object.
9
9
  * @param {Array} findings
10
10
  * @param {string[]} [exempted=[]]
11
+ * @param {object} [config={}] - loaded config; security_officer stamped when set
11
12
  * @returns {object}
12
13
  */
13
- function toJson(findings, exempted = []) {
14
+ function toJson(findings, exempted = [], config = {}) {
14
15
  const real = findings.filter(f => !f.likelyFalsePositive);
15
16
  const noisy = findings.filter(f => f.likelyFalsePositive);
16
17
 
17
18
  const summary = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
18
19
  for (const f of real) summary[f.severity] = (summary[f.severity] || 0) + 1;
19
20
 
20
- return {
21
+ const envelope = {
21
22
  version,
22
23
  summary,
23
24
  findings: real,
@@ -25,6 +26,9 @@ function toJson(findings, exempted = []) {
25
26
  exempted,
26
27
  scannedAt: new Date().toISOString(),
27
28
  };
29
+ if (config.security_name) envelope.security_name = config.security_name;
30
+ if (config.security_email) envelope.security_email = config.security_email;
31
+ return envelope;
28
32
  }
29
33
 
30
34
  // ─── SARIF ────────────────────────────────────────────────────────────────────
@@ -62,6 +66,17 @@ const CWE_MAP = {
62
66
 
63
67
  const NPM_URL = 'https://www.npmjs.com/package/@lhi/tdd-audit';
64
68
 
69
+ function safeSiteUrl(raw) {
70
+ if (!raw || !raw.trim()) return NPM_URL;
71
+ try {
72
+ const parsed = new URL(raw.trim());
73
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return NPM_URL;
74
+ return raw.trim();
75
+ } catch {
76
+ return NPM_URL;
77
+ }
78
+ }
79
+
65
80
  /**
66
81
  * Return findings as a SARIF 2.1.0 object (GitHub code scanning compatible).
67
82
  * @param {Array} findings
@@ -113,7 +128,7 @@ function toSarif(findings, projectDir = '', config = {}) {
113
128
  driver: {
114
129
  name: config.badge_label || '@lhi/tdd-audit',
115
130
  version,
116
- informationUri: (config.tdd_site && config.tdd_site.trim()) ? config.tdd_site.trim() : NPM_URL,
131
+ informationUri: safeSiteUrl(config.tdd_site),
117
132
  rules,
118
133
  },
119
134
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Security skill installer for Claude Code, Gemini CLI, Cursor, Codex, and OpenCode. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,13 +19,124 @@ If the user passes `--scan` or `--scan-only`, requests "audit only", or asks for
19
19
 
20
20
  ---
21
21
 
22
+ ## PR Mode (`--pr`)
23
+
24
+ Lightweight, fast path designed for CI PR gates. When invoked with `--pr`:
25
+
26
+ 1. Run Phase 0 static scan only (no AI agents, no RAG queries, no code changes).
27
+ 2. Filter findings against `severityThreshold` (default `HIGH`).
28
+ 3. Apply any `severity_overrides` from `.tdd-audit.json` before filtering.
29
+ 4. If any finding meets or exceeds the threshold: exit non-zero with a summary. Otherwise exit zero.
30
+
31
+ Output format in PR mode:
32
+ ```
33
+ tdd-audit PR scan — my-project
34
+ ✅ 0 CRITICAL · 0 HIGH (threshold: HIGH) — passed
35
+ ```
36
+ or:
37
+ ```
38
+ tdd-audit PR scan — my-project
39
+ ❌ 1 CRITICAL · 2 HIGH (threshold: HIGH) — blocked
40
+ CRITICAL src/api/admin.js:14 Unguarded admin endpoint
41
+ HIGH src/lib/auth.js:88 JWT algorithm confusion
42
+ ```
43
+
44
+ Do not start MCP services, pull pattern repos, or run agents in this mode. Speed is the goal.
45
+
46
+ ---
47
+
48
+ ## Org Scan Mode (`--org <github-org>`)
49
+
50
+ Scans all repos in a GitHub org. When invoked with `--org`:
51
+
52
+ 1. List all repos in `<github-org>` via `gh repo list <github-org> --limit 200 --json name,sshUrl`.
53
+ 2. For each repo: clone to a temp dir (or pull if already present), run `--pr` mode against it.
54
+ 3. Collect results and produce a cross-org summary:
55
+
56
+ ```
57
+ <github-org> security posture — YYYY-MM-DD
58
+
59
+ ✅ repo-a 0 critical · 0 high
60
+ ⚠️ repo-b 0 critical · 2 high
61
+ 🔴 repo-c 1 critical · 4 high
62
+
63
+ N repos scanned · X critical · Y high total
64
+ ```
65
+
66
+ 4. If `webhook_url` or `slack_webhook` is configured, fire the notification with the aggregate payload.
67
+ 5. If `--format report` is also passed, write a full markdown cross-org report.
68
+
69
+ Requires `GITHUB_TOKEN` in the environment with `repo` read scope.
70
+
71
+ ---
72
+
73
+ ## Auto-Fix PR Mode (`--open-pr`)
74
+
75
+ Instead of committing fixes directly to the working branch, open a GitHub PR per confirmed finding. Apply this mode during Phase 1–3 (Remediation Engine):
76
+
77
+ For each finding:
78
+ 1. Create a branch: `tdd-audit/<finding-slug>-<YYYYMMDD>` off the default branch.
79
+ 2. Apply the Red (exploit test) + Green (patch) commits on that branch.
80
+ 3. Open a PR via `gh pr create`:
81
+ - Title: `[tdd-audit] Fix <vulnerability name>: <one-line description>`
82
+ - Body: finding description, exploit test name, patch summary, link to vulnerability pattern.
83
+ 4. Do **not** merge — leave the PR for human review.
84
+ 5. Print the PR URL after creation.
85
+
86
+ Requires `GITHUB_TOKEN` (env or `github_token` config) and `github_repo` (env or auto-detected from git remote).
87
+
88
+ ---
89
+
90
+ ## Watch Mode (`--watch`)
91
+
92
+ Re-scan affected files on save. When invoked with `--watch`:
93
+
94
+ 1. Complete Phase 0 (full static scan) once at startup.
95
+ 2. Start a file watcher on the repo root (excluding `node_modules`, `dist`, `.git`, and paths in `ignore`).
96
+ 3. On any file save: re-run Phase 0 static scan for that file only.
97
+ 4. Report new or resolved findings immediately in the terminal. Do not run agents or apply fixes.
98
+ 5. Continue watching until the process is terminated.
99
+
100
+ Watch mode is for real-time feedback during development. Use `/caller-audit` (or the equivalent skill command) for full agentic remediation.
101
+
102
+ ---
103
+
104
+ ## Notifications
105
+
106
+ After every completed scan (CLI, `--ai`, `POST /scan`):
107
+
108
+ **Webhook** (`webhook_url`): POST the following JSON:
109
+ ```json
110
+ {
111
+ "project": "<project>",
112
+ "org": "<org>",
113
+ "security_name": "<security_name or omitted if not set>",
114
+ "security_email": "<security_email or omitted if not set>",
115
+ "timestamp": "<ISO 8601>",
116
+ "duration_ms": 4200,
117
+ "summary": { "critical": 1, "high": 3, "medium": 2, "low": 0 },
118
+ "findings": [ ... ]
119
+ }
120
+ ```
121
+
122
+ **Slack** (`slack_webhook`): Send a message to `slack_channel` (or the webhook default):
123
+ ```
124
+ 🔴 tdd-audit — <project>
125
+ 1 critical · 3 high · 2 medium
126
+ Run /caller-audit to remediate.
127
+ ```
128
+
129
+ Send notifications only after Phase 0e (findings are final). Do not send during incremental watch-mode scans.
130
+
131
+ ---
132
+
22
133
  ## Config Bootstrap (runs before Phase 0 every time)
23
134
 
24
135
  Before scanning, read `.tdd-audit.json` from the repo root if it exists. Store the values — they control branding, extensibility, and session setup for this run.
25
136
 
26
137
  ```
27
138
  If .tdd-audit.json exists:
28
- Load: org, project, tdd_site, badge_label,
139
+ Load: org, project, tdd_site, badge_label, security_name, security_email,
29
140
  pattern_repos, extra_skill_dirs, extra_repos,
30
141
  mcp_services, extra_domains
31
142
  If absent:
@@ -588,6 +699,21 @@ Once coverage is ≥ 95%, add a coverage badge to `README.md`.
588
699
 
589
700
  Adjust the percentage in the badge URL to match the real number (e.g., `97%25` for 97%).
590
701
 
702
+ **Badge label and link defaults:**
703
+
704
+ - If `badge_label` is set in config, use it as the label (e.g., `dc-audit`). Otherwise use `tdd-audit`.
705
+ - If `tdd_site` is set in config, link the badge to that URL. Otherwise link to the `@lhi/tdd-audit` npm page (`https://www.npmjs.com/package/@lhi/tdd-audit`).
706
+
707
+ ```markdown
708
+ <!-- default (no config overrides) -->
709
+ [![tdd-audit](https://img.shields.io/badge/tdd--audit-passing-brightgreen)](https://www.npmjs.com/package/@lhi/tdd-audit)
710
+
711
+ <!-- with badge_label and tdd_site set -->
712
+ [![dc-audit](https://img.shields.io/badge/dc--audit-passing-brightgreen)](https://security.example.com)
713
+ ```
714
+
715
+ The `<!-- tdd-audit-badge -->` HTML comment must follow the badge line so it can be located and updated on subsequent runs.
716
+
591
717
  ---
592
718
 
593
719
  ## Phase 6: SECURITY.md
@@ -613,7 +739,7 @@ Please **do not** open a public GitHub issue for security vulnerabilities.
613
739
 
614
740
  Report vulnerabilities privately via:
615
741
  - **GitHub**: Use [GitHub's private vulnerability reporting](../../security/advisories/new)
616
- - **Email**: security@example.com *(replace with project contact)*
742
+ - **Contact**: <if security_name and security_email both set: "Name <email>"; if only email: email; if only name: name; if neither: "security@example.com (replace with project contact)">
617
743
 
618
744
  Expect acknowledgement within **48 hours** and a patch or mitigation plan within **14 days** for verified HIGH/CRITICAL issues. Reporters are credited in release notes unless anonymity is requested.
619
745
 
@@ -633,6 +759,63 @@ Replace placeholder email and version table with the project's real information.
633
759
 
634
760
  ---
635
761
 
762
+ ## Phase 6b: SBOM (`--sbom`)
763
+
764
+ If `sbom: true` in config or `--sbom` flag is passed, generate a [CycloneDX](https://cyclonedx.org/) Software Bill of Materials after the dependency audit:
765
+
766
+ ```bash
767
+ # Node.js
768
+ npx @cyclonedx/cyclonedx-npm --output-file sbom.json
769
+
770
+ # Python
771
+ cyclonedx-py --output sbom.json
772
+
773
+ # Go
774
+ cyclonedx-gomod app -output sbom.json
775
+ ```
776
+
777
+ Write to `sbom.json` at the project root. Note the path in the Final Report.
778
+
779
+ ---
780
+
781
+ ## Phase 6c: Compliance Report (`--format report`)
782
+
783
+ If `report: true` in config or `--format report` flag is passed, generate a markdown compliance report at `audit-report.md`:
784
+
785
+ ```markdown
786
+ # Security Audit Report — <project> — <YYYY-MM-DD>
787
+
788
+ **Org:** <org> **Auditor:** tdd-audit **Security Contact:** <security_name if set, security_email if set, or N/A> **Status:** Passed / Failed
789
+
790
+ ## Findings Summary
791
+ | Severity | Count | Status |
792
+ |---|---|---|
793
+ | CRITICAL | 0 | ✅ Remediated |
794
+ | HIGH | 2 | ✅ Remediated |
795
+ | MEDIUM | 1 | ✅ Remediated |
796
+ | LOW | 0 | — |
797
+
798
+ ## Fix Evidence
799
+ | Vulnerability | Exploit Test | Patch Commit | Suite |
800
+ |---|---|---|---|
801
+ | JWT algorithm confusion | auth-jwt-alg.test.js | abc1234 | ✅ |
802
+
803
+ ## Coverage Gate
804
+ Line: 96.4% ✅ Branch: 95.1% ✅ Threshold: 95%
805
+
806
+ ## Hardening Controls Applied
807
+ - Security headers (Helmet / CSP)
808
+ - Rate limiting on auth routes
809
+ - Dependency audit passed
810
+
811
+ ## SBOM
812
+ sbom.json (CycloneDX 1.4) — generated <timestamp>
813
+ ```
814
+
815
+ Suitable for attaching to SOC 2 audits, ISO 27001 evidence packages, and vendor security questionnaires.
816
+
817
+ ---
818
+
636
819
  ## Final Report
637
820
 
638
821
  After Phases 4–6 complete, append to the Remediation Summary:
@@ -642,10 +825,14 @@ After Phases 4–6 complete, append to the Remediation Summary:
642
825
 
643
826
  | Item | Status | Detail |
644
827
  |---|---|---|
645
- | Line coverage | ✅ | 96.4% |
646
- | Branch coverage | ✅ | 95.1% |
647
- | README badge | ✅ | Updated to 96% (brightgreen) |
648
- | SECURITY.md | ✅ | Created at repo root |
828
+ | Line coverage | ✅ | 96.4% |
829
+ | Branch coverage | ✅ | 95.1% |
830
+ | README badge | ✅ | Updated to 96% (brightgreen) |
831
+ | SECURITY.md | ✅ | Created at repo root |
832
+ | SBOM | ✅/⏭ | sbom.json (CycloneDX) generated — or N/A if --sbom not passed |
833
+ | Compliance report | ✅/⏭ | audit-report.md generated — or N/A if --format report not passed |
834
+ | Notifications fired | ✅/⏭ | webhook + Slack — or N/A if not configured |
835
+ | Patterns contributed| ✅/⏭ | N new patterns to <pattern_repo.name> — or "existing patterns verified" |
649
836
  ```
650
837
 
651
838
  ---