javi-forge 1.5.0 → 1.6.1

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.
Files changed (217) hide show
  1. package/README.md +191 -3
  2. package/ci-local/hooks/pre-push +17 -13
  3. package/dist/commands/analyze.d.ts +1 -1
  4. package/dist/commands/analyze.js +15 -15
  5. package/dist/commands/atlassian-mcp.d.ts +42 -0
  6. package/dist/commands/atlassian-mcp.js +98 -0
  7. package/dist/commands/ci.d.ts +3 -3
  8. package/dist/commands/ci.js +185 -147
  9. package/dist/commands/crash-recovery.d.ts +34 -0
  10. package/dist/commands/crash-recovery.js +123 -0
  11. package/dist/commands/doctor.d.ts +2 -2
  12. package/dist/commands/doctor.js +113 -61
  13. package/dist/commands/harness-audit.d.ts +35 -0
  14. package/dist/commands/harness-audit.js +277 -0
  15. package/dist/commands/init.d.ts +1 -1
  16. package/dist/commands/init.js +415 -118
  17. package/dist/commands/llmstxt.d.ts +1 -1
  18. package/dist/commands/llmstxt.js +36 -34
  19. package/dist/commands/parallel-batch.d.ts +42 -0
  20. package/dist/commands/parallel-batch.js +90 -0
  21. package/dist/commands/plugin.d.ts +26 -1
  22. package/dist/commands/plugin.js +138 -24
  23. package/dist/commands/secret-scanner.d.ts +30 -0
  24. package/dist/commands/secret-scanner.js +272 -0
  25. package/dist/commands/security-analysis.d.ts +74 -0
  26. package/dist/commands/security-analysis.js +487 -0
  27. package/dist/commands/security.d.ts +31 -0
  28. package/dist/commands/security.js +445 -0
  29. package/dist/commands/skill-scanner.d.ts +63 -0
  30. package/dist/commands/skill-scanner.js +383 -0
  31. package/dist/commands/skills.d.ts +139 -0
  32. package/dist/commands/skills.js +895 -0
  33. package/dist/commands/supply-chain.d.ts +23 -0
  34. package/dist/commands/supply-chain.js +126 -0
  35. package/dist/commands/tdd-pipeline.d.ts +17 -0
  36. package/dist/commands/tdd-pipeline.js +144 -0
  37. package/dist/commands/tdd.d.ts +21 -0
  38. package/dist/commands/tdd.js +120 -0
  39. package/dist/commands/team-presets.d.ts +53 -0
  40. package/dist/commands/team-presets.js +201 -0
  41. package/dist/commands/workflow.d.ts +23 -0
  42. package/dist/commands/workflow.js +114 -0
  43. package/dist/constants.d.ts +21 -0
  44. package/dist/constants.js +208 -37
  45. package/dist/index.js +400 -54
  46. package/dist/lib/agent-skills.d.ts +73 -0
  47. package/dist/lib/agent-skills.js +260 -0
  48. package/dist/lib/auto-skill-install.d.ts +37 -0
  49. package/dist/lib/auto-skill-install.js +92 -0
  50. package/dist/lib/auto-wire.d.ts +20 -0
  51. package/dist/lib/auto-wire.js +240 -0
  52. package/dist/lib/claudemd.d.ts +20 -0
  53. package/dist/lib/claudemd.js +222 -0
  54. package/dist/lib/codex-export.d.ts +16 -0
  55. package/dist/lib/codex-export.js +109 -0
  56. package/dist/lib/common.d.ts +1 -1
  57. package/dist/lib/common.js +52 -44
  58. package/dist/lib/context.d.ts +27 -0
  59. package/dist/lib/context.js +204 -0
  60. package/dist/lib/docker.d.ts +1 -1
  61. package/dist/lib/docker.js +141 -112
  62. package/dist/lib/frontmatter.d.ts +1 -1
  63. package/dist/lib/frontmatter.js +29 -15
  64. package/dist/lib/plugin.d.ts +19 -1
  65. package/dist/lib/plugin.js +174 -47
  66. package/dist/lib/skill-publish.d.ts +40 -0
  67. package/dist/lib/skill-publish.js +146 -0
  68. package/dist/lib/stack-detector.d.ts +38 -0
  69. package/dist/lib/stack-detector.js +207 -0
  70. package/dist/lib/template.d.ts +16 -1
  71. package/dist/lib/template.js +46 -17
  72. package/dist/lib/workflow/discovery.d.ts +19 -0
  73. package/dist/lib/workflow/discovery.js +68 -0
  74. package/dist/lib/workflow/index.d.ts +5 -0
  75. package/dist/lib/workflow/index.js +5 -0
  76. package/dist/lib/workflow/parser.d.ts +16 -0
  77. package/dist/lib/workflow/parser.js +198 -0
  78. package/dist/lib/workflow/renderer.d.ts +9 -0
  79. package/dist/lib/workflow/renderer.js +152 -0
  80. package/dist/lib/workflow/validator.d.ts +10 -0
  81. package/dist/lib/workflow/validator.js +189 -0
  82. package/dist/tasks/index.d.ts +4 -0
  83. package/dist/tasks/index.js +4 -0
  84. package/dist/tasks/scaffold-tasks.d.ts +3 -0
  85. package/dist/tasks/scaffold-tasks.js +14 -0
  86. package/dist/tasks/task-id.d.ts +30 -0
  87. package/dist/tasks/task-id.js +55 -0
  88. package/dist/tasks/task-tracker.d.ts +15 -0
  89. package/dist/tasks/task-tracker.js +81 -0
  90. package/dist/types/index.d.ts +252 -5
  91. package/dist/types/index.js +11 -1
  92. package/dist/ui/AnalyzeUI.d.ts +1 -1
  93. package/dist/ui/AnalyzeUI.js +38 -39
  94. package/dist/ui/App.d.ts +5 -3
  95. package/dist/ui/App.js +92 -46
  96. package/dist/ui/AutoSkills.d.ts +9 -0
  97. package/dist/ui/AutoSkills.js +124 -0
  98. package/dist/ui/CI.d.ts +2 -2
  99. package/dist/ui/CI.js +24 -26
  100. package/dist/ui/CIContext.d.ts +1 -1
  101. package/dist/ui/CIContext.js +3 -2
  102. package/dist/ui/CISelector.d.ts +2 -2
  103. package/dist/ui/CISelector.js +23 -15
  104. package/dist/ui/Doctor.d.ts +1 -1
  105. package/dist/ui/Doctor.js +35 -29
  106. package/dist/ui/Header.d.ts +1 -1
  107. package/dist/ui/Header.js +14 -14
  108. package/dist/ui/HookProfileSelector.d.ts +9 -0
  109. package/dist/ui/HookProfileSelector.js +54 -0
  110. package/dist/ui/LlmsTxt.d.ts +1 -1
  111. package/dist/ui/LlmsTxt.js +31 -22
  112. package/dist/ui/MemorySelector.d.ts +2 -2
  113. package/dist/ui/MemorySelector.js +28 -16
  114. package/dist/ui/NameInput.d.ts +1 -1
  115. package/dist/ui/NameInput.js +21 -21
  116. package/dist/ui/OptionSelector.d.ts +8 -2
  117. package/dist/ui/OptionSelector.js +83 -26
  118. package/dist/ui/Plugin.d.ts +4 -3
  119. package/dist/ui/Plugin.js +89 -29
  120. package/dist/ui/Progress.d.ts +3 -3
  121. package/dist/ui/Progress.js +23 -22
  122. package/dist/ui/Skills.d.ts +11 -0
  123. package/dist/ui/Skills.js +148 -0
  124. package/dist/ui/StackSelector.d.ts +2 -2
  125. package/dist/ui/StackSelector.js +26 -16
  126. package/dist/ui/Summary.d.ts +3 -3
  127. package/dist/ui/Summary.js +60 -50
  128. package/dist/ui/Welcome.d.ts +1 -1
  129. package/dist/ui/Welcome.js +15 -16
  130. package/dist/ui/theme.d.ts +1 -1
  131. package/dist/ui/theme.js +6 -6
  132. package/package.json +9 -6
  133. package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
  134. package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
  135. package/templates/common/repoforge/repoforge.yaml +34 -0
  136. package/templates/github/deploy-docker-zero-downtime.yml +140 -0
  137. package/templates/github/repoforge-graph.yml +45 -0
  138. package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
  139. package/templates/local-ai/.env.example +17 -0
  140. package/templates/local-ai/docker-compose.yml +95 -0
  141. package/templates/security-hooks/claude-settings-security.json +30 -0
  142. package/templates/security-hooks/commit-msg-signing +29 -0
  143. package/templates/security-hooks/pre-commit-permissions +74 -0
  144. package/templates/security-hooks/pre-commit-secrets +74 -0
  145. package/templates/security-hooks/pre-push-branch-protection +62 -0
  146. package/templates/security-hooks/pre-push-deps +83 -0
  147. package/templates/security-hooks/pre-push-signing +67 -0
  148. package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
  149. package/templates/workflows/ci-pipeline.dot +15 -0
  150. package/templates/workflows/feature-flow.dot +21 -0
  151. package/templates/workflows/release.dot +16 -0
  152. package/dist/__integration__/helpers.d.ts +0 -20
  153. package/dist/__integration__/helpers.d.ts.map +0 -1
  154. package/dist/__integration__/helpers.js +0 -31
  155. package/dist/__integration__/helpers.js.map +0 -1
  156. package/dist/commands/analyze.d.ts.map +0 -1
  157. package/dist/commands/analyze.js.map +0 -1
  158. package/dist/commands/ci.d.ts.map +0 -1
  159. package/dist/commands/ci.js.map +0 -1
  160. package/dist/commands/doctor.d.ts.map +0 -1
  161. package/dist/commands/doctor.js.map +0 -1
  162. package/dist/commands/init.d.ts.map +0 -1
  163. package/dist/commands/init.js.map +0 -1
  164. package/dist/commands/llmstxt.d.ts.map +0 -1
  165. package/dist/commands/llmstxt.js.map +0 -1
  166. package/dist/commands/plugin.d.ts.map +0 -1
  167. package/dist/commands/plugin.js.map +0 -1
  168. package/dist/constants.d.ts.map +0 -1
  169. package/dist/constants.js.map +0 -1
  170. package/dist/index.d.ts.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/lib/common.d.ts.map +0 -1
  173. package/dist/lib/common.js.map +0 -1
  174. package/dist/lib/docker.d.ts.map +0 -1
  175. package/dist/lib/docker.js.map +0 -1
  176. package/dist/lib/frontmatter.d.ts.map +0 -1
  177. package/dist/lib/frontmatter.js.map +0 -1
  178. package/dist/lib/plugin.d.ts.map +0 -1
  179. package/dist/lib/plugin.js.map +0 -1
  180. package/dist/lib/template.d.ts.map +0 -1
  181. package/dist/lib/template.js.map +0 -1
  182. package/dist/types/index.d.ts.map +0 -1
  183. package/dist/types/index.js.map +0 -1
  184. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  185. package/dist/ui/AnalyzeUI.js.map +0 -1
  186. package/dist/ui/App.d.ts.map +0 -1
  187. package/dist/ui/App.js.map +0 -1
  188. package/dist/ui/CI.d.ts.map +0 -1
  189. package/dist/ui/CI.js.map +0 -1
  190. package/dist/ui/CIContext.d.ts.map +0 -1
  191. package/dist/ui/CIContext.js.map +0 -1
  192. package/dist/ui/CISelector.d.ts.map +0 -1
  193. package/dist/ui/CISelector.js.map +0 -1
  194. package/dist/ui/Doctor.d.ts.map +0 -1
  195. package/dist/ui/Doctor.js.map +0 -1
  196. package/dist/ui/Header.d.ts.map +0 -1
  197. package/dist/ui/Header.js.map +0 -1
  198. package/dist/ui/LlmsTxt.d.ts.map +0 -1
  199. package/dist/ui/LlmsTxt.js.map +0 -1
  200. package/dist/ui/MemorySelector.d.ts.map +0 -1
  201. package/dist/ui/MemorySelector.js.map +0 -1
  202. package/dist/ui/NameInput.d.ts.map +0 -1
  203. package/dist/ui/NameInput.js.map +0 -1
  204. package/dist/ui/OptionSelector.d.ts.map +0 -1
  205. package/dist/ui/OptionSelector.js.map +0 -1
  206. package/dist/ui/Plugin.d.ts.map +0 -1
  207. package/dist/ui/Plugin.js.map +0 -1
  208. package/dist/ui/Progress.d.ts.map +0 -1
  209. package/dist/ui/Progress.js.map +0 -1
  210. package/dist/ui/StackSelector.d.ts.map +0 -1
  211. package/dist/ui/StackSelector.js.map +0 -1
  212. package/dist/ui/Summary.d.ts.map +0 -1
  213. package/dist/ui/Summary.js.map +0 -1
  214. package/dist/ui/Welcome.d.ts.map +0 -1
  215. package/dist/ui/Welcome.js.map +0 -1
  216. package/dist/ui/theme.d.ts.map +0 -1
  217. package/dist/ui/theme.js.map +0 -1
@@ -0,0 +1,445 @@
1
+ import { execFile } from "child_process";
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ import { promisify } from "util";
5
+ import { detectCIStack } from "./ci.js";
6
+ const execFileAsync = promisify(execFile);
7
+ // =============================================================================
8
+ // Constants
9
+ // =============================================================================
10
+ const BASELINE_DIR = ".javi-forge";
11
+ const BASELINE_FILE = "security-baseline.json";
12
+ const BASELINE_VERSION = "2.0.0";
13
+ const SEVERITY_ORDER = {
14
+ critical: 5,
15
+ high: 4,
16
+ moderate: 3,
17
+ low: 2,
18
+ info: 1,
19
+ };
20
+ const DEFAULT_STALE_DAYS = 30;
21
+ // =============================================================================
22
+ // Audit command resolution
23
+ // =============================================================================
24
+ export function getAuditCommand(stack, buildTool) {
25
+ switch (stack) {
26
+ case "node":
27
+ switch (buildTool) {
28
+ case "pnpm":
29
+ return { cmd: "pnpm", args: ["audit", "--json"] };
30
+ case "yarn":
31
+ return { cmd: "yarn", args: ["npm", "audit", "--json"] };
32
+ default:
33
+ return { cmd: "npm", args: ["audit", "--json"] };
34
+ }
35
+ case "python":
36
+ return { cmd: "pip-audit", args: ["--format=json", "--output=-"] };
37
+ case "go":
38
+ return { cmd: "govulncheck", args: ["-json", "./..."] };
39
+ case "rust":
40
+ return { cmd: "cargo", args: ["audit", "--json"] };
41
+ default:
42
+ return null;
43
+ }
44
+ }
45
+ // =============================================================================
46
+ // Audit output parsing
47
+ // =============================================================================
48
+ export function makeFindingKey(finding) {
49
+ return `${finding.id}:${finding.package}`;
50
+ }
51
+ export function parseNpmAudit(raw) {
52
+ const findings = [];
53
+ try {
54
+ const data = JSON.parse(raw);
55
+ // npm audit v2 JSON format: { vulnerabilities: { [name]: { ... } } }
56
+ const vulns = data.vulnerabilities ?? {};
57
+ for (const [pkgName, info] of Object.entries(vulns)) {
58
+ const v = info;
59
+ // via can contain objects (direct vulns) or strings (transitive refs)
60
+ const directVias = (v.via ?? []).filter((x) => typeof x === "object");
61
+ if (directVias.length === 0) {
62
+ findings.push({
63
+ id: `npm-${pkgName}`,
64
+ severity: normalizeSeverity(v.severity),
65
+ package: pkgName,
66
+ title: `Vulnerability in ${pkgName}`,
67
+ });
68
+ }
69
+ else {
70
+ for (const via of directVias) {
71
+ findings.push({
72
+ id: via.source ? `GHSA-${via.source}` : `npm-${pkgName}`,
73
+ severity: normalizeSeverity(v.severity),
74
+ package: pkgName,
75
+ title: via.title ?? `Vulnerability in ${pkgName}`,
76
+ url: via.url,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ // If JSON parse fails, return empty — audit tool may not be available
84
+ }
85
+ return findings;
86
+ }
87
+ export function parsePipAudit(raw) {
88
+ const findings = [];
89
+ try {
90
+ const data = JSON.parse(raw);
91
+ // pip-audit JSON: array of { name, version, vulns: [{ id, fix_versions, description }] }
92
+ const deps = Array.isArray(data) ? data : (data.dependencies ?? []);
93
+ for (const dep of deps) {
94
+ for (const vuln of dep.vulns ?? []) {
95
+ findings.push({
96
+ id: vuln.id ?? `pip-${dep.name}`,
97
+ severity: normalizeSeverity(vuln.fix_versions?.length ? "high" : "moderate"),
98
+ package: dep.name,
99
+ title: vuln.description ?? `Vulnerability in ${dep.name}`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ catch {
105
+ // empty
106
+ }
107
+ return findings;
108
+ }
109
+ export function parseCargoAudit(raw) {
110
+ const findings = [];
111
+ try {
112
+ const data = JSON.parse(raw);
113
+ const vulns = data.vulnerabilities?.list ?? [];
114
+ for (const v of vulns) {
115
+ const advisory = v.advisory ?? {};
116
+ findings.push({
117
+ id: advisory.id ?? `cargo-${v.package?.name ?? "unknown"}`,
118
+ severity: normalizeSeverity(advisory.cvss?.severity),
119
+ package: v.package?.name ?? "unknown",
120
+ title: advisory.title ?? `Vulnerability in ${v.package?.name ?? "unknown"}`,
121
+ url: advisory.url,
122
+ });
123
+ }
124
+ }
125
+ catch {
126
+ // empty
127
+ }
128
+ return findings;
129
+ }
130
+ export function parseGovulncheck(raw) {
131
+ const findings = [];
132
+ try {
133
+ // govulncheck JSON outputs one JSON object per line (NDJSON)
134
+ const lines = raw.split("\n").filter((l) => l.trim());
135
+ for (const line of lines) {
136
+ try {
137
+ const entry = JSON.parse(line);
138
+ if (entry.osv) {
139
+ findings.push({
140
+ id: entry.osv.id ?? "unknown",
141
+ severity: normalizeSeverity(entry.osv.database_specific?.severity),
142
+ package: entry.osv.affected?.[0]?.package?.name ?? "unknown",
143
+ title: entry.osv.summary ?? entry.osv.id ?? "Go vulnerability",
144
+ url: entry.osv.references?.[0]?.url,
145
+ });
146
+ }
147
+ }
148
+ catch {
149
+ // skip non-JSON lines
150
+ }
151
+ }
152
+ }
153
+ catch {
154
+ // empty
155
+ }
156
+ return findings;
157
+ }
158
+ function normalizeSeverity(raw) {
159
+ if (!raw)
160
+ return "moderate";
161
+ const lower = raw.toLowerCase();
162
+ if (lower === "critical")
163
+ return "critical";
164
+ if (lower === "high")
165
+ return "high";
166
+ if (lower === "moderate" || lower === "medium")
167
+ return "moderate";
168
+ if (lower === "low")
169
+ return "low";
170
+ if (lower === "info" || lower === "none")
171
+ return "info";
172
+ return "moderate";
173
+ }
174
+ export function parseAuditOutput(stack, raw) {
175
+ switch (stack) {
176
+ case "node":
177
+ return parseNpmAudit(raw);
178
+ case "python":
179
+ return parsePipAudit(raw);
180
+ case "rust":
181
+ return parseCargoAudit(raw);
182
+ case "go":
183
+ return parseGovulncheck(raw);
184
+ default:
185
+ return [];
186
+ }
187
+ }
188
+ // =============================================================================
189
+ // Severity helpers
190
+ // =============================================================================
191
+ export function severityAtOrAbove(severity, threshold) {
192
+ return SEVERITY_ORDER[severity] >= SEVERITY_ORDER[threshold];
193
+ }
194
+ export function filterBySeverity(findings, minSeverity) {
195
+ return findings.filter((f) => severityAtOrAbove(f.severity, minSeverity));
196
+ }
197
+ // =============================================================================
198
+ // Allowlist filtering
199
+ // =============================================================================
200
+ export function filterAllowlisted(findings, allowlist) {
201
+ if (allowlist.length === 0)
202
+ return findings;
203
+ const allowSet = new Set(allowlist);
204
+ return findings.filter((f) => !allowSet.has(makeFindingKey(f)));
205
+ }
206
+ // =============================================================================
207
+ // Staleness detection
208
+ // =============================================================================
209
+ export function checkStaleness(baseline, staleDays) {
210
+ const refDate = baseline.updatedAt ?? baseline.createdAt;
211
+ const ageMs = Date.now() - new Date(refDate).getTime();
212
+ const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
213
+ if (ageDays > staleDays) {
214
+ return `Baseline is ${ageDays} days old (threshold: ${staleDays}). Consider running \`javi-forge security update\`.`;
215
+ }
216
+ return undefined;
217
+ }
218
+ export function baselineAgeDays(baseline) {
219
+ const refDate = baseline.updatedAt ?? baseline.createdAt;
220
+ const ageMs = Date.now() - new Date(refDate).getTime();
221
+ return Math.floor(ageMs / (1000 * 60 * 60 * 24));
222
+ }
223
+ // =============================================================================
224
+ // Summary computation
225
+ // =============================================================================
226
+ export function computeSummary(current, regressions, resolved, filteredRegressions, baseline) {
227
+ const bySeverity = {
228
+ critical: 0,
229
+ high: 0,
230
+ moderate: 0,
231
+ low: 0,
232
+ info: 0,
233
+ };
234
+ for (const f of current) {
235
+ bySeverity[f.severity]++;
236
+ }
237
+ return {
238
+ total: current.length,
239
+ bySeverity,
240
+ regressionCount: regressions.length,
241
+ resolvedCount: resolved.length,
242
+ filteredCount: filteredRegressions.length,
243
+ baselineAge: baselineAgeDays(baseline),
244
+ };
245
+ }
246
+ // =============================================================================
247
+ // Regression detection
248
+ // =============================================================================
249
+ export function detectRegressions(baseline, current, options = {}) {
250
+ const baselineKeySet = new Set(baseline.findingKeys);
251
+ const currentKeys = current.map(makeFindingKey);
252
+ const currentKeySet = new Set(currentKeys);
253
+ let regressions = current.filter((f) => !baselineKeySet.has(makeFindingKey(f)));
254
+ const resolved = baseline.findings.filter((f) => !currentKeySet.has(makeFindingKey(f)));
255
+ // Apply allowlist filtering
256
+ const allowlist = baseline.allowlist ?? [];
257
+ regressions = filterAllowlisted(regressions, allowlist);
258
+ // Apply severity threshold
259
+ const minSeverity = options.minSeverity ?? "low";
260
+ const filteredRegressions = filterBySeverity(regressions, minSeverity);
261
+ // Check staleness
262
+ const staleDays = options.staleDays ?? DEFAULT_STALE_DAYS;
263
+ const staleWarning = checkStaleness(baseline, staleDays);
264
+ const summary = computeSummary(current, regressions, resolved, filteredRegressions, baseline);
265
+ return {
266
+ baseline,
267
+ current,
268
+ regressions,
269
+ resolved,
270
+ filteredRegressions,
271
+ staleWarning,
272
+ summary,
273
+ };
274
+ }
275
+ // =============================================================================
276
+ // Baseline file I/O
277
+ // =============================================================================
278
+ function baselinePath(projectDir) {
279
+ return path.join(projectDir, BASELINE_DIR, BASELINE_FILE);
280
+ }
281
+ export async function readBaseline(projectDir) {
282
+ const bp = baselinePath(projectDir);
283
+ if (!(await fs.pathExists(bp)))
284
+ return null;
285
+ try {
286
+ return await fs.readJson(bp);
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
292
+ export async function writeBaseline(projectDir, baseline) {
293
+ const bp = baselinePath(projectDir);
294
+ await fs.ensureDir(path.dirname(bp));
295
+ await fs.writeJson(bp, baseline, { spaces: 2 });
296
+ }
297
+ // =============================================================================
298
+ // Run audit tool
299
+ // =============================================================================
300
+ async function runAuditTool(projectDir, auditCmd) {
301
+ try {
302
+ const { stdout } = await execFileAsync(auditCmd.cmd, auditCmd.args, {
303
+ cwd: projectDir,
304
+ timeout: 120_000,
305
+ maxBuffer: 10 * 1024 * 1024,
306
+ });
307
+ return stdout;
308
+ }
309
+ catch (err) {
310
+ // npm audit exits non-zero when vulns are found — that's expected
311
+ // We still want the stdout (JSON output)
312
+ if (err && typeof err === "object" && "stdout" in err) {
313
+ const stdout = err.stdout;
314
+ if (stdout && stdout.trim().length > 0)
315
+ return stdout;
316
+ }
317
+ throw err;
318
+ }
319
+ }
320
+ // =============================================================================
321
+ // Main security commands
322
+ // =============================================================================
323
+ function report(onStep, id, label, status, detail) {
324
+ onStep({ id, label, status, detail });
325
+ }
326
+ export async function runSecurity(mode, projectDir, onStep, options = {}) {
327
+ // ── Detect stack ────────────────────────────────────────────────────────
328
+ report(onStep, "detect", "Detecting stack", "running");
329
+ let stackInfo;
330
+ try {
331
+ stackInfo = await detectCIStack(projectDir);
332
+ report(onStep, "detect", `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, "done");
333
+ }
334
+ catch (e) {
335
+ report(onStep, "detect", "Detecting stack", "error", String(e));
336
+ throw e;
337
+ }
338
+ // ── Resolve audit command ──────────────────────────────────────────────
339
+ const auditCmd = getAuditCommand(stackInfo.stackType, stackInfo.buildTool);
340
+ if (!auditCmd) {
341
+ report(onStep, "audit", "Security audit", "error", `No audit tool for stack "${stackInfo.stackType}". Supported: node, python, go, rust`);
342
+ throw new Error(`Unsupported stack for security audit: ${stackInfo.stackType}`);
343
+ }
344
+ // ── Run audit ──────────────────────────────────────────────────────────
345
+ report(onStep, "audit", `Running ${auditCmd.cmd} audit`, "running");
346
+ let raw;
347
+ try {
348
+ raw = await runAuditTool(projectDir, auditCmd);
349
+ report(onStep, "audit", `Audit complete`, "done");
350
+ }
351
+ catch (e) {
352
+ report(onStep, "audit", `Audit failed`, "error", `${auditCmd.cmd} not found or failed. Install it first.`);
353
+ throw e;
354
+ }
355
+ // ── Parse findings ─────────────────────────────────────────────────────
356
+ const findings = parseAuditOutput(stackInfo.stackType, raw);
357
+ switch (mode) {
358
+ case "baseline":
359
+ case "update": {
360
+ report(onStep, "save", mode === "update" ? "Updating baseline" : "Creating baseline", "running");
361
+ // Preserve createdAt and allowlist on update
362
+ let createdAt = new Date().toISOString();
363
+ let allowlist = [];
364
+ if (mode === "update") {
365
+ const existing = await readBaseline(projectDir);
366
+ if (existing) {
367
+ createdAt = existing.createdAt;
368
+ allowlist = existing.allowlist ?? [];
369
+ }
370
+ }
371
+ const baseline = {
372
+ version: BASELINE_VERSION,
373
+ createdAt,
374
+ updatedAt: mode === "update" ? new Date().toISOString() : undefined,
375
+ stack: stackInfo.stackType,
376
+ buildTool: stackInfo.buildTool,
377
+ findings,
378
+ findingKeys: findings.map(makeFindingKey),
379
+ allowlist,
380
+ };
381
+ await writeBaseline(projectDir, baseline);
382
+ report(onStep, "save", `Baseline saved with ${findings.length} finding(s)`, "done", baselinePath(projectDir));
383
+ return null;
384
+ }
385
+ case "check": {
386
+ report(onStep, "check", "Checking for regressions", "running");
387
+ const existing = await readBaseline(projectDir);
388
+ if (!existing) {
389
+ report(onStep, "check", "No baseline found", "error", "Run `javi-forge security baseline` first to create a baseline");
390
+ throw new Error("No security baseline found. Run `javi-forge security baseline` first.");
391
+ }
392
+ const result = detectRegressions(existing, findings, options);
393
+ // Staleness warning
394
+ if (result.staleWarning) {
395
+ report(onStep, "stale", "Baseline staleness", "skipped", result.staleWarning);
396
+ }
397
+ // Summary line
398
+ const { summary } = result;
399
+ const sevBreakdown = Object.entries(summary.bySeverity)
400
+ .filter(([, count]) => count > 0)
401
+ .map(([sev, count]) => `${count} ${sev}`)
402
+ .join(", ");
403
+ if (sevBreakdown) {
404
+ report(onStep, "summary", `Current findings: ${summary.total} (${sevBreakdown})`, "done");
405
+ }
406
+ // Use filteredRegressions (severity-filtered + allowlist-filtered) for pass/fail
407
+ if (result.filteredRegressions.length === 0) {
408
+ const resolvedMsg = result.resolved.length > 0
409
+ ? ` (${result.resolved.length} resolved)`
410
+ : "";
411
+ const belowThreshold = result.regressions.length > result.filteredRegressions.length
412
+ ? ` (${result.regressions.length - result.filteredRegressions.length} below threshold)`
413
+ : "";
414
+ report(onStep, "check", `No actionable regressions${resolvedMsg}${belowThreshold}`, "done");
415
+ }
416
+ else {
417
+ const details = result.filteredRegressions
418
+ .map((r) => ` ${r.severity.toUpperCase()} ${r.package}: ${r.title}`)
419
+ .join("\n");
420
+ report(onStep, "check", `${result.filteredRegressions.length} regression(s) found`, "error", details);
421
+ }
422
+ return result;
423
+ }
424
+ case "allowlist": {
425
+ report(onStep, "allowlist", "Updating allowlist", "running");
426
+ const existing = await readBaseline(projectDir);
427
+ if (!existing) {
428
+ report(onStep, "allowlist", "No baseline found", "error", "Run `javi-forge security baseline` first to create a baseline");
429
+ throw new Error("No security baseline found. Run `javi-forge security baseline` first.");
430
+ }
431
+ // Add all current findings to the allowlist
432
+ const currentKeys = findings.map(makeFindingKey);
433
+ const existingAllowlist = new Set(existing.allowlist ?? []);
434
+ for (const key of currentKeys) {
435
+ existingAllowlist.add(key);
436
+ }
437
+ existing.allowlist = [...existingAllowlist];
438
+ existing.updatedAt = new Date().toISOString();
439
+ await writeBaseline(projectDir, existing);
440
+ report(onStep, "allowlist", `Allowlist updated: ${existingAllowlist.size} finding(s) allowed`, "done", baselinePath(projectDir));
441
+ return null;
442
+ }
443
+ }
444
+ }
445
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * CI-level skill security scanner — pre-install analysis of SKILL.md files.
3
+ *
4
+ * Detects credential theft, code injection, data exfiltration, and scope escape
5
+ * patterns before skills are installed. Inspired by the SkillGuard skill
6
+ * (javi-ai) but implemented as a programmatic module with structured output.
7
+ */
8
+ import type { SecuritySeverity } from "../types/index.js";
9
+ export type SkillThreatCategory = "credential-theft" | "code-injection" | "data-exfiltration" | "scope-escape" | "privilege-escalation" | "destructive-command" | "self-modification" | "hook-tampering" | "obfuscation" | "missing-provenance" | "excessive-permissions" | "file-traversal";
10
+ export interface SkillThreat {
11
+ category: SkillThreatCategory;
12
+ severity: SecuritySeverity;
13
+ pattern: string;
14
+ line: number;
15
+ context: string;
16
+ message: string;
17
+ }
18
+ export type SkillScanVerdict = "pass" | "warn" | "block";
19
+ export interface SkillScanResult {
20
+ skillPath: string;
21
+ skillName: string;
22
+ verdict: SkillScanVerdict;
23
+ threats: SkillThreat[];
24
+ summary: SkillScanSummary;
25
+ }
26
+ export interface SkillScanSummary {
27
+ total: number;
28
+ critical: number;
29
+ high: number;
30
+ moderate: number;
31
+ low: number;
32
+ }
33
+ interface ThreatPattern {
34
+ category: SkillThreatCategory;
35
+ severity: SecuritySeverity;
36
+ pattern: RegExp;
37
+ message: string;
38
+ }
39
+ /**
40
+ * Ordered by severity (critical first). Each pattern is tested against
41
+ * every non-comment line in the skill file.
42
+ */
43
+ export declare const THREAT_PATTERNS: ThreatPattern[];
44
+ interface ProvenanceInfo {
45
+ hasAuthor: boolean;
46
+ hasVersion: boolean;
47
+ hasDescription: boolean;
48
+ }
49
+ export declare function checkProvenance(content: string): ProvenanceInfo;
50
+ export declare function scanSkillContent(content: string, filePath: string): SkillThreat[];
51
+ export declare function computeVerdict(threats: SkillThreat[]): SkillScanVerdict;
52
+ export declare function computeScanSummary(threats: SkillThreat[]): SkillScanSummary;
53
+ export declare function extractSkillName(content: string, filePath: string): string;
54
+ export declare function scanSkillFile(filePath: string): Promise<SkillScanResult>;
55
+ /**
56
+ * Scan all SKILL.md files in a directory (recursive).
57
+ * Useful for scanning a plugin's skills directory before installation.
58
+ */
59
+ export declare function scanSkillsDirectory(dir: string): Promise<SkillScanResult[]>;
60
+ export declare function formatScanReport(result: SkillScanResult): string;
61
+ export declare function formatBatchReport(results: SkillScanResult[]): string;
62
+ export {};
63
+ //# sourceMappingURL=skill-scanner.d.ts.map