javi-forge 1.6.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 (231) hide show
  1. package/dist/commands/analyze.d.ts +1 -1
  2. package/dist/commands/analyze.js +15 -15
  3. package/dist/commands/atlassian-mcp.d.ts +42 -0
  4. package/dist/commands/atlassian-mcp.js +98 -0
  5. package/dist/commands/ci.d.ts +3 -3
  6. package/dist/commands/ci.js +185 -147
  7. package/dist/commands/crash-recovery.d.ts +34 -0
  8. package/dist/commands/crash-recovery.js +123 -0
  9. package/dist/commands/doctor.d.ts +2 -2
  10. package/dist/commands/doctor.js +113 -61
  11. package/dist/commands/harness-audit.d.ts +35 -0
  12. package/dist/commands/harness-audit.js +277 -0
  13. package/dist/commands/init.d.ts +1 -1
  14. package/dist/commands/init.js +384 -141
  15. package/dist/commands/llmstxt.d.ts +1 -1
  16. package/dist/commands/llmstxt.js +36 -34
  17. package/dist/commands/parallel-batch.d.ts +42 -0
  18. package/dist/commands/parallel-batch.js +90 -0
  19. package/dist/commands/plugin.d.ts +10 -1
  20. package/dist/commands/plugin.js +92 -47
  21. package/dist/commands/secret-scanner.d.ts +30 -0
  22. package/dist/commands/secret-scanner.js +272 -0
  23. package/dist/commands/security-analysis.d.ts +74 -0
  24. package/dist/commands/security-analysis.js +487 -0
  25. package/dist/commands/security.d.ts +11 -5
  26. package/dist/commands/security.js +216 -76
  27. package/dist/commands/skill-scanner.d.ts +63 -0
  28. package/dist/commands/skill-scanner.js +383 -0
  29. package/dist/commands/skills.d.ts +62 -5
  30. package/dist/commands/skills.js +439 -54
  31. package/dist/commands/supply-chain.d.ts +23 -0
  32. package/dist/commands/supply-chain.js +126 -0
  33. package/dist/commands/tdd-pipeline.d.ts +17 -0
  34. package/dist/commands/tdd-pipeline.js +144 -0
  35. package/dist/commands/tdd.d.ts +1 -1
  36. package/dist/commands/tdd.js +21 -18
  37. package/dist/commands/team-presets.d.ts +53 -0
  38. package/dist/commands/team-presets.js +201 -0
  39. package/dist/commands/workflow.d.ts +23 -0
  40. package/dist/commands/workflow.js +114 -0
  41. package/dist/constants.d.ts +15 -1
  42. package/dist/constants.js +161 -122
  43. package/dist/index.js +308 -98
  44. package/dist/lib/agent-skills.d.ts +36 -1
  45. package/dist/lib/agent-skills.js +168 -19
  46. package/dist/lib/auto-skill-install.d.ts +37 -0
  47. package/dist/lib/auto-skill-install.js +92 -0
  48. package/dist/lib/auto-wire.d.ts +20 -0
  49. package/dist/lib/auto-wire.js +240 -0
  50. package/dist/lib/claudemd.d.ts +13 -1
  51. package/dist/lib/claudemd.js +174 -24
  52. package/dist/lib/codex-export.d.ts +1 -1
  53. package/dist/lib/codex-export.js +29 -31
  54. package/dist/lib/common.d.ts +1 -1
  55. package/dist/lib/common.js +52 -44
  56. package/dist/lib/context.d.ts +17 -2
  57. package/dist/lib/context.js +142 -13
  58. package/dist/lib/docker.d.ts +1 -1
  59. package/dist/lib/docker.js +141 -112
  60. package/dist/lib/frontmatter.d.ts +1 -1
  61. package/dist/lib/frontmatter.js +29 -15
  62. package/dist/lib/plugin.d.ts +9 -3
  63. package/dist/lib/plugin.js +128 -69
  64. package/dist/lib/skill-publish.d.ts +40 -0
  65. package/dist/lib/skill-publish.js +146 -0
  66. package/dist/lib/stack-detector.d.ts +38 -0
  67. package/dist/lib/stack-detector.js +207 -0
  68. package/dist/lib/template.d.ts +16 -1
  69. package/dist/lib/template.js +46 -17
  70. package/dist/lib/workflow/discovery.d.ts +19 -0
  71. package/dist/lib/workflow/discovery.js +68 -0
  72. package/dist/lib/workflow/index.d.ts +5 -0
  73. package/dist/lib/workflow/index.js +5 -0
  74. package/dist/lib/workflow/parser.d.ts +16 -0
  75. package/dist/lib/workflow/parser.js +198 -0
  76. package/dist/lib/workflow/renderer.d.ts +9 -0
  77. package/dist/lib/workflow/renderer.js +152 -0
  78. package/dist/lib/workflow/validator.d.ts +10 -0
  79. package/dist/lib/workflow/validator.js +189 -0
  80. package/dist/tasks/index.d.ts +4 -0
  81. package/dist/tasks/index.js +4 -0
  82. package/dist/tasks/scaffold-tasks.d.ts +3 -0
  83. package/dist/tasks/scaffold-tasks.js +14 -0
  84. package/dist/tasks/task-id.d.ts +30 -0
  85. package/dist/tasks/task-id.js +55 -0
  86. package/dist/tasks/task-tracker.d.ts +15 -0
  87. package/dist/tasks/task-tracker.js +81 -0
  88. package/dist/types/index.d.ts +134 -6
  89. package/dist/types/index.js +11 -1
  90. package/dist/ui/AnalyzeUI.d.ts +1 -1
  91. package/dist/ui/AnalyzeUI.js +38 -39
  92. package/dist/ui/App.d.ts +5 -3
  93. package/dist/ui/App.js +86 -46
  94. package/dist/ui/AutoSkills.d.ts +9 -0
  95. package/dist/ui/AutoSkills.js +124 -0
  96. package/dist/ui/CI.d.ts +2 -2
  97. package/dist/ui/CI.js +24 -26
  98. package/dist/ui/CIContext.d.ts +1 -1
  99. package/dist/ui/CIContext.js +3 -2
  100. package/dist/ui/CISelector.d.ts +2 -2
  101. package/dist/ui/CISelector.js +23 -15
  102. package/dist/ui/Doctor.d.ts +1 -1
  103. package/dist/ui/Doctor.js +35 -29
  104. package/dist/ui/Header.d.ts +1 -1
  105. package/dist/ui/Header.js +14 -14
  106. package/dist/ui/HookProfileSelector.d.ts +9 -0
  107. package/dist/ui/HookProfileSelector.js +54 -0
  108. package/dist/ui/LlmsTxt.d.ts +1 -1
  109. package/dist/ui/LlmsTxt.js +31 -22
  110. package/dist/ui/MemorySelector.d.ts +2 -2
  111. package/dist/ui/MemorySelector.js +28 -16
  112. package/dist/ui/NameInput.d.ts +1 -1
  113. package/dist/ui/NameInput.js +21 -21
  114. package/dist/ui/OptionSelector.d.ts +6 -2
  115. package/dist/ui/OptionSelector.js +83 -32
  116. package/dist/ui/Plugin.d.ts +4 -3
  117. package/dist/ui/Plugin.js +78 -35
  118. package/dist/ui/Progress.d.ts +3 -3
  119. package/dist/ui/Progress.js +23 -22
  120. package/dist/ui/Skills.d.ts +2 -2
  121. package/dist/ui/Skills.js +61 -32
  122. package/dist/ui/StackSelector.d.ts +2 -2
  123. package/dist/ui/StackSelector.js +26 -16
  124. package/dist/ui/Summary.d.ts +3 -3
  125. package/dist/ui/Summary.js +60 -50
  126. package/dist/ui/Welcome.d.ts +1 -1
  127. package/dist/ui/Welcome.js +15 -16
  128. package/dist/ui/theme.d.ts +1 -1
  129. package/dist/ui/theme.js +6 -6
  130. package/package.json +9 -6
  131. package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
  132. package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
  133. package/templates/common/repoforge/repoforge.yaml +34 -0
  134. package/templates/github/deploy-docker-zero-downtime.yml +140 -0
  135. package/templates/github/repoforge-graph.yml +45 -0
  136. package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
  137. package/templates/local-ai/.env.example +17 -0
  138. package/templates/local-ai/docker-compose.yml +95 -0
  139. package/templates/security-hooks/claude-settings-security.json +30 -0
  140. package/templates/security-hooks/commit-msg-signing +29 -0
  141. package/templates/security-hooks/pre-commit-permissions +74 -0
  142. package/templates/security-hooks/pre-commit-secrets +74 -0
  143. package/templates/security-hooks/pre-push-branch-protection +62 -0
  144. package/templates/security-hooks/pre-push-deps +83 -0
  145. package/templates/security-hooks/pre-push-signing +67 -0
  146. package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
  147. package/templates/workflows/ci-pipeline.dot +15 -0
  148. package/templates/workflows/feature-flow.dot +21 -0
  149. package/templates/workflows/release.dot +16 -0
  150. package/dist/__integration__/helpers.d.ts +0 -20
  151. package/dist/__integration__/helpers.d.ts.map +0 -1
  152. package/dist/__integration__/helpers.js +0 -31
  153. package/dist/__integration__/helpers.js.map +0 -1
  154. package/dist/commands/analyze.d.ts.map +0 -1
  155. package/dist/commands/analyze.js.map +0 -1
  156. package/dist/commands/ci.d.ts.map +0 -1
  157. package/dist/commands/ci.js.map +0 -1
  158. package/dist/commands/doctor.d.ts.map +0 -1
  159. package/dist/commands/doctor.js.map +0 -1
  160. package/dist/commands/init.d.ts.map +0 -1
  161. package/dist/commands/init.js.map +0 -1
  162. package/dist/commands/llmstxt.d.ts.map +0 -1
  163. package/dist/commands/llmstxt.js.map +0 -1
  164. package/dist/commands/plugin.d.ts.map +0 -1
  165. package/dist/commands/plugin.js.map +0 -1
  166. package/dist/commands/security.d.ts.map +0 -1
  167. package/dist/commands/security.js.map +0 -1
  168. package/dist/commands/skills.d.ts.map +0 -1
  169. package/dist/commands/skills.js.map +0 -1
  170. package/dist/commands/tdd.d.ts.map +0 -1
  171. package/dist/commands/tdd.js.map +0 -1
  172. package/dist/constants.d.ts.map +0 -1
  173. package/dist/constants.js.map +0 -1
  174. package/dist/index.d.ts.map +0 -1
  175. package/dist/index.js.map +0 -1
  176. package/dist/lib/agent-skills.d.ts.map +0 -1
  177. package/dist/lib/agent-skills.js.map +0 -1
  178. package/dist/lib/claudemd.d.ts.map +0 -1
  179. package/dist/lib/claudemd.js.map +0 -1
  180. package/dist/lib/codex-export.d.ts.map +0 -1
  181. package/dist/lib/codex-export.js.map +0 -1
  182. package/dist/lib/common.d.ts.map +0 -1
  183. package/dist/lib/common.js.map +0 -1
  184. package/dist/lib/context.d.ts.map +0 -1
  185. package/dist/lib/context.js.map +0 -1
  186. package/dist/lib/docker.d.ts.map +0 -1
  187. package/dist/lib/docker.js.map +0 -1
  188. package/dist/lib/frontmatter.d.ts.map +0 -1
  189. package/dist/lib/frontmatter.js.map +0 -1
  190. package/dist/lib/plugin.d.ts.map +0 -1
  191. package/dist/lib/plugin.js.map +0 -1
  192. package/dist/lib/template.d.ts.map +0 -1
  193. package/dist/lib/template.js.map +0 -1
  194. package/dist/types/index.d.ts.map +0 -1
  195. package/dist/types/index.js.map +0 -1
  196. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  197. package/dist/ui/AnalyzeUI.js.map +0 -1
  198. package/dist/ui/App.d.ts.map +0 -1
  199. package/dist/ui/App.js.map +0 -1
  200. package/dist/ui/CI.d.ts.map +0 -1
  201. package/dist/ui/CI.js.map +0 -1
  202. package/dist/ui/CIContext.d.ts.map +0 -1
  203. package/dist/ui/CIContext.js.map +0 -1
  204. package/dist/ui/CISelector.d.ts.map +0 -1
  205. package/dist/ui/CISelector.js.map +0 -1
  206. package/dist/ui/Doctor.d.ts.map +0 -1
  207. package/dist/ui/Doctor.js.map +0 -1
  208. package/dist/ui/Header.d.ts.map +0 -1
  209. package/dist/ui/Header.js.map +0 -1
  210. package/dist/ui/LlmsTxt.d.ts.map +0 -1
  211. package/dist/ui/LlmsTxt.js.map +0 -1
  212. package/dist/ui/MemorySelector.d.ts.map +0 -1
  213. package/dist/ui/MemorySelector.js.map +0 -1
  214. package/dist/ui/NameInput.d.ts.map +0 -1
  215. package/dist/ui/NameInput.js.map +0 -1
  216. package/dist/ui/OptionSelector.d.ts.map +0 -1
  217. package/dist/ui/OptionSelector.js.map +0 -1
  218. package/dist/ui/Plugin.d.ts.map +0 -1
  219. package/dist/ui/Plugin.js.map +0 -1
  220. package/dist/ui/Progress.d.ts.map +0 -1
  221. package/dist/ui/Progress.js.map +0 -1
  222. package/dist/ui/Skills.d.ts.map +0 -1
  223. package/dist/ui/Skills.js.map +0 -1
  224. package/dist/ui/StackSelector.d.ts.map +0 -1
  225. package/dist/ui/StackSelector.js.map +0 -1
  226. package/dist/ui/Summary.d.ts.map +0 -1
  227. package/dist/ui/Summary.js.map +0 -1
  228. package/dist/ui/Welcome.d.ts.map +0 -1
  229. package/dist/ui/Welcome.js.map +0 -1
  230. package/dist/ui/theme.d.ts.map +0 -1
  231. package/dist/ui/theme.js.map +0 -1
@@ -1,32 +1,43 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
- import { detectCIStack } from './ci.js';
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
6
  const execFileAsync = promisify(execFile);
7
7
  // =============================================================================
8
8
  // Constants
9
9
  // =============================================================================
10
- const BASELINE_DIR = '.javi-forge';
11
- const BASELINE_FILE = 'security-baseline.json';
12
- const BASELINE_VERSION = '1.0.0';
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;
13
21
  // =============================================================================
14
22
  // Audit command resolution
15
23
  // =============================================================================
16
24
  export function getAuditCommand(stack, buildTool) {
17
25
  switch (stack) {
18
- case 'node':
26
+ case "node":
19
27
  switch (buildTool) {
20
- case 'pnpm': return { cmd: 'pnpm', args: ['audit', '--json'] };
21
- case 'yarn': return { cmd: 'yarn', args: ['npm', 'audit', '--json'] };
22
- default: return { cmd: 'npm', args: ['audit', '--json'] };
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"] };
23
34
  }
24
- case 'python':
25
- return { cmd: 'pip-audit', args: ['--format=json', '--output=-'] };
26
- case 'go':
27
- return { cmd: 'govulncheck', args: ['-json', './...'] };
28
- case 'rust':
29
- return { cmd: 'cargo', args: ['audit', '--json'] };
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"] };
30
41
  default:
31
42
  return null;
32
43
  }
@@ -46,7 +57,7 @@ export function parseNpmAudit(raw) {
46
57
  for (const [pkgName, info] of Object.entries(vulns)) {
47
58
  const v = info;
48
59
  // via can contain objects (direct vulns) or strings (transitive refs)
49
- const directVias = (v.via ?? []).filter((x) => typeof x === 'object');
60
+ const directVias = (v.via ?? []).filter((x) => typeof x === "object");
50
61
  if (directVias.length === 0) {
51
62
  findings.push({
52
63
  id: `npm-${pkgName}`,
@@ -80,10 +91,10 @@ export function parsePipAudit(raw) {
80
91
  // pip-audit JSON: array of { name, version, vulns: [{ id, fix_versions, description }] }
81
92
  const deps = Array.isArray(data) ? data : (data.dependencies ?? []);
82
93
  for (const dep of deps) {
83
- for (const vuln of (dep.vulns ?? [])) {
94
+ for (const vuln of dep.vulns ?? []) {
84
95
  findings.push({
85
96
  id: vuln.id ?? `pip-${dep.name}`,
86
- severity: normalizeSeverity(vuln.fix_versions?.length ? 'high' : 'moderate'),
97
+ severity: normalizeSeverity(vuln.fix_versions?.length ? "high" : "moderate"),
87
98
  package: dep.name,
88
99
  title: vuln.description ?? `Vulnerability in ${dep.name}`,
89
100
  });
@@ -103,10 +114,10 @@ export function parseCargoAudit(raw) {
103
114
  for (const v of vulns) {
104
115
  const advisory = v.advisory ?? {};
105
116
  findings.push({
106
- id: advisory.id ?? `cargo-${v.package?.name ?? 'unknown'}`,
117
+ id: advisory.id ?? `cargo-${v.package?.name ?? "unknown"}`,
107
118
  severity: normalizeSeverity(advisory.cvss?.severity),
108
- package: v.package?.name ?? 'unknown',
109
- title: advisory.title ?? `Vulnerability in ${v.package?.name ?? 'unknown'}`,
119
+ package: v.package?.name ?? "unknown",
120
+ title: advisory.title ?? `Vulnerability in ${v.package?.name ?? "unknown"}`,
110
121
  url: advisory.url,
111
122
  });
112
123
  }
@@ -120,16 +131,16 @@ export function parseGovulncheck(raw) {
120
131
  const findings = [];
121
132
  try {
122
133
  // govulncheck JSON outputs one JSON object per line (NDJSON)
123
- const lines = raw.split('\n').filter(l => l.trim());
134
+ const lines = raw.split("\n").filter((l) => l.trim());
124
135
  for (const line of lines) {
125
136
  try {
126
137
  const entry = JSON.parse(line);
127
138
  if (entry.osv) {
128
139
  findings.push({
129
- id: entry.osv.id ?? 'unknown',
140
+ id: entry.osv.id ?? "unknown",
130
141
  severity: normalizeSeverity(entry.osv.database_specific?.severity),
131
- package: entry.osv.affected?.[0]?.package?.name ?? 'unknown',
132
- title: entry.osv.summary ?? entry.osv.id ?? 'Go vulnerability',
142
+ package: entry.osv.affected?.[0]?.package?.name ?? "unknown",
143
+ title: entry.osv.summary ?? entry.osv.id ?? "Go vulnerability",
133
144
  url: entry.osv.references?.[0]?.url,
134
145
  });
135
146
  }
@@ -146,39 +157,120 @@ export function parseGovulncheck(raw) {
146
157
  }
147
158
  function normalizeSeverity(raw) {
148
159
  if (!raw)
149
- return 'moderate';
160
+ return "moderate";
150
161
  const lower = raw.toLowerCase();
151
- if (lower === 'critical')
152
- return 'critical';
153
- if (lower === 'high')
154
- return 'high';
155
- if (lower === 'moderate' || lower === 'medium')
156
- return 'moderate';
157
- if (lower === 'low')
158
- return 'low';
159
- if (lower === 'info' || lower === 'none')
160
- return 'info';
161
- return 'moderate';
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";
162
173
  }
163
174
  export function parseAuditOutput(stack, raw) {
164
175
  switch (stack) {
165
- case 'node': return parseNpmAudit(raw);
166
- case 'python': return parsePipAudit(raw);
167
- case 'rust': return parseCargoAudit(raw);
168
- case 'go': return parseGovulncheck(raw);
169
- default: return [];
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\`.`;
170
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
+ };
171
245
  }
172
246
  // =============================================================================
173
247
  // Regression detection
174
248
  // =============================================================================
175
- export function detectRegressions(baseline, current) {
249
+ export function detectRegressions(baseline, current, options = {}) {
176
250
  const baselineKeySet = new Set(baseline.findingKeys);
177
251
  const currentKeys = current.map(makeFindingKey);
178
252
  const currentKeySet = new Set(currentKeys);
179
- const regressions = current.filter(f => !baselineKeySet.has(makeFindingKey(f)));
180
- const resolved = baseline.findings.filter(f => !currentKeySet.has(makeFindingKey(f)));
181
- return { baseline, current, regressions, resolved };
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
+ };
182
274
  }
183
275
  // =============================================================================
184
276
  // Baseline file I/O
@@ -188,7 +280,7 @@ function baselinePath(projectDir) {
188
280
  }
189
281
  export async function readBaseline(projectDir) {
190
282
  const bp = baselinePath(projectDir);
191
- if (!await fs.pathExists(bp))
283
+ if (!(await fs.pathExists(bp)))
192
284
  return null;
193
285
  try {
194
286
  return await fs.readJson(bp);
@@ -217,7 +309,7 @@ async function runAuditTool(projectDir, auditCmd) {
217
309
  catch (err) {
218
310
  // npm audit exits non-zero when vulns are found — that's expected
219
311
  // We still want the stdout (JSON output)
220
- if (err && typeof err === 'object' && 'stdout' in err) {
312
+ if (err && typeof err === "object" && "stdout" in err) {
221
313
  const stdout = err.stdout;
222
314
  if (stdout && stdout.trim().length > 0)
223
315
  return stdout;
@@ -231,75 +323,123 @@ async function runAuditTool(projectDir, auditCmd) {
231
323
  function report(onStep, id, label, status, detail) {
232
324
  onStep({ id, label, status, detail });
233
325
  }
234
- export async function runSecurity(mode, projectDir, onStep) {
326
+ export async function runSecurity(mode, projectDir, onStep, options = {}) {
235
327
  // ── Detect stack ────────────────────────────────────────────────────────
236
- report(onStep, 'detect', 'Detecting stack', 'running');
328
+ report(onStep, "detect", "Detecting stack", "running");
237
329
  let stackInfo;
238
330
  try {
239
331
  stackInfo = await detectCIStack(projectDir);
240
- report(onStep, 'detect', `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, 'done');
332
+ report(onStep, "detect", `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, "done");
241
333
  }
242
334
  catch (e) {
243
- report(onStep, 'detect', 'Detecting stack', 'error', String(e));
335
+ report(onStep, "detect", "Detecting stack", "error", String(e));
244
336
  throw e;
245
337
  }
246
338
  // ── Resolve audit command ──────────────────────────────────────────────
247
339
  const auditCmd = getAuditCommand(stackInfo.stackType, stackInfo.buildTool);
248
340
  if (!auditCmd) {
249
- report(onStep, 'audit', 'Security audit', 'error', `No audit tool for stack "${stackInfo.stackType}". Supported: node, python, go, rust`);
341
+ report(onStep, "audit", "Security audit", "error", `No audit tool for stack "${stackInfo.stackType}". Supported: node, python, go, rust`);
250
342
  throw new Error(`Unsupported stack for security audit: ${stackInfo.stackType}`);
251
343
  }
252
344
  // ── Run audit ──────────────────────────────────────────────────────────
253
- report(onStep, 'audit', `Running ${auditCmd.cmd} audit`, 'running');
345
+ report(onStep, "audit", `Running ${auditCmd.cmd} audit`, "running");
254
346
  let raw;
255
347
  try {
256
348
  raw = await runAuditTool(projectDir, auditCmd);
257
- report(onStep, 'audit', `Audit complete`, 'done');
349
+ report(onStep, "audit", `Audit complete`, "done");
258
350
  }
259
351
  catch (e) {
260
- report(onStep, 'audit', `Audit failed`, 'error', `${auditCmd.cmd} not found or failed. Install it first.`);
352
+ report(onStep, "audit", `Audit failed`, "error", `${auditCmd.cmd} not found or failed. Install it first.`);
261
353
  throw e;
262
354
  }
263
355
  // ── Parse findings ─────────────────────────────────────────────────────
264
356
  const findings = parseAuditOutput(stackInfo.stackType, raw);
265
357
  switch (mode) {
266
- case 'baseline':
267
- case 'update': {
268
- report(onStep, 'save', mode === 'update' ? 'Updating baseline' : 'Creating baseline', 'running');
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
+ }
269
371
  const baseline = {
270
372
  version: BASELINE_VERSION,
271
- createdAt: new Date().toISOString(),
373
+ createdAt,
374
+ updatedAt: mode === "update" ? new Date().toISOString() : undefined,
272
375
  stack: stackInfo.stackType,
273
376
  buildTool: stackInfo.buildTool,
274
377
  findings,
275
378
  findingKeys: findings.map(makeFindingKey),
379
+ allowlist,
276
380
  };
277
381
  await writeBaseline(projectDir, baseline);
278
- report(onStep, 'save', `Baseline saved with ${findings.length} finding(s)`, 'done', baselinePath(projectDir));
382
+ report(onStep, "save", `Baseline saved with ${findings.length} finding(s)`, "done", baselinePath(projectDir));
279
383
  return null;
280
384
  }
281
- case 'check': {
282
- report(onStep, 'check', 'Checking for regressions', 'running');
385
+ case "check": {
386
+ report(onStep, "check", "Checking for regressions", "running");
283
387
  const existing = await readBaseline(projectDir);
284
388
  if (!existing) {
285
- report(onStep, 'check', 'No baseline found', 'error', 'Run `javi-forge security baseline` first to create a baseline');
286
- throw new Error('No security baseline found. Run `javi-forge security baseline` first.');
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");
287
405
  }
288
- const result = detectRegressions(existing, findings);
289
- if (result.regressions.length === 0) {
406
+ // Use filteredRegressions (severity-filtered + allowlist-filtered) for pass/fail
407
+ if (result.filteredRegressions.length === 0) {
290
408
  const resolvedMsg = result.resolved.length > 0
291
409
  ? ` (${result.resolved.length} resolved)`
292
- : '';
293
- report(onStep, 'check', `No new vulnerabilities${resolvedMsg}`, 'done');
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");
294
415
  }
295
416
  else {
296
- const details = result.regressions
297
- .map(r => ` ${r.severity.toUpperCase()} ${r.package}: ${r.title}`)
298
- .join('\n');
299
- report(onStep, 'check', `${result.regressions.length} regression(s) found`, 'error', details);
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);
300
421
  }
301
422
  return result;
302
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
+ }
303
443
  }
304
444
  }
305
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