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.
- package/README.md +191 -3
- package/ci-local/hooks/pre-push +17 -13
- package/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +15 -15
- package/dist/commands/atlassian-mcp.d.ts +42 -0
- package/dist/commands/atlassian-mcp.js +98 -0
- package/dist/commands/ci.d.ts +3 -3
- package/dist/commands/ci.js +185 -147
- package/dist/commands/crash-recovery.d.ts +34 -0
- package/dist/commands/crash-recovery.js +123 -0
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +113 -61
- package/dist/commands/harness-audit.d.ts +35 -0
- package/dist/commands/harness-audit.js +277 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +415 -118
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/parallel-batch.d.ts +42 -0
- package/dist/commands/parallel-batch.js +90 -0
- package/dist/commands/plugin.d.ts +26 -1
- package/dist/commands/plugin.js +138 -24
- package/dist/commands/secret-scanner.d.ts +30 -0
- package/dist/commands/secret-scanner.js +272 -0
- package/dist/commands/security-analysis.d.ts +74 -0
- package/dist/commands/security-analysis.js +487 -0
- package/dist/commands/security.d.ts +31 -0
- package/dist/commands/security.js +445 -0
- package/dist/commands/skill-scanner.d.ts +63 -0
- package/dist/commands/skill-scanner.js +383 -0
- package/dist/commands/skills.d.ts +139 -0
- package/dist/commands/skills.js +895 -0
- package/dist/commands/supply-chain.d.ts +23 -0
- package/dist/commands/supply-chain.js +126 -0
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +21 -0
- package/dist/commands/tdd.js +120 -0
- package/dist/commands/team-presets.d.ts +53 -0
- package/dist/commands/team-presets.js +201 -0
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +114 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +208 -37
- package/dist/index.js +400 -54
- package/dist/lib/agent-skills.d.ts +73 -0
- package/dist/lib/agent-skills.js +260 -0
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +92 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/claudemd.d.ts +20 -0
- package/dist/lib/claudemd.js +222 -0
- package/dist/lib/codex-export.d.ts +16 -0
- package/dist/lib/codex-export.js +109 -0
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +27 -0
- package/dist/lib/context.js +204 -0
- package/dist/lib/docker.d.ts +1 -1
- package/dist/lib/docker.js +141 -112
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/plugin.d.ts +19 -1
- package/dist/lib/plugin.js +174 -47
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +146 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +19 -0
- package/dist/lib/workflow/discovery.js +68 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +55 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +252 -5
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +92 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +3 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +8 -2
- package/dist/ui/OptionSelector.js +83 -26
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +89 -29
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +11 -0
- package/dist/ui/Skills.js +148 -0
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +26 -16
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/package.json +9 -6
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CI.d.ts.map +0 -1
- package/dist/ui/CI.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- 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
|