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.
- 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 +384 -141
- 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 +10 -1
- package/dist/commands/plugin.js +92 -47
- 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 +11 -5
- package/dist/commands/security.js +216 -76
- package/dist/commands/skill-scanner.d.ts +63 -0
- package/dist/commands/skill-scanner.js +383 -0
- package/dist/commands/skills.d.ts +62 -5
- package/dist/commands/skills.js +439 -54
- 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 +1 -1
- package/dist/commands/tdd.js +21 -18
- 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 +15 -1
- package/dist/constants.js +161 -122
- package/dist/index.js +308 -98
- package/dist/lib/agent-skills.d.ts +36 -1
- package/dist/lib/agent-skills.js +168 -19
- 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 +13 -1
- package/dist/lib/claudemd.js +174 -24
- package/dist/lib/codex-export.d.ts +1 -1
- package/dist/lib/codex-export.js +29 -31
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +17 -2
- package/dist/lib/context.js +142 -13
- 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 +9 -3
- package/dist/lib/plugin.js +128 -69
- 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 +134 -6
- 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 +86 -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 +6 -2
- package/dist/ui/OptionSelector.js +83 -32
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +78 -35
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +2 -2
- package/dist/ui/Skills.js +61 -32
- 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/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/skills.d.ts.map +0 -1
- package/dist/commands/skills.js.map +0 -1
- package/dist/commands/tdd.d.ts.map +0 -1
- package/dist/commands/tdd.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/agent-skills.d.ts.map +0 -1
- package/dist/lib/agent-skills.js.map +0 -1
- package/dist/lib/claudemd.d.ts.map +0 -1
- package/dist/lib/claudemd.js.map +0 -1
- package/dist/lib/codex-export.d.ts.map +0 -1
- package/dist/lib/codex-export.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.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/Skills.d.ts.map +0 -1
- package/dist/ui/Skills.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
|
@@ -1,32 +1,43 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { detectCIStack } from
|
|
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 =
|
|
11
|
-
const BASELINE_FILE =
|
|
12
|
-
const BASELINE_VERSION =
|
|
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
|
|
26
|
+
case "node":
|
|
19
27
|
switch (buildTool) {
|
|
20
|
-
case
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
return { cmd:
|
|
26
|
-
case
|
|
27
|
-
return { cmd:
|
|
28
|
-
case
|
|
29
|
-
return { cmd:
|
|
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 ===
|
|
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
|
|
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 ?
|
|
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 ??
|
|
117
|
+
id: advisory.id ?? `cargo-${v.package?.name ?? "unknown"}`,
|
|
107
118
|
severity: normalizeSeverity(advisory.cvss?.severity),
|
|
108
|
-
package: v.package?.name ??
|
|
109
|
-
title: advisory.title ?? `Vulnerability in ${v.package?.name ??
|
|
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(
|
|
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 ??
|
|
140
|
+
id: entry.osv.id ?? "unknown",
|
|
130
141
|
severity: normalizeSeverity(entry.osv.database_specific?.severity),
|
|
131
|
-
package: entry.osv.affected?.[0]?.package?.name ??
|
|
132
|
-
title: entry.osv.summary ?? entry.osv.id ??
|
|
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
|
|
160
|
+
return "moderate";
|
|
150
161
|
const lower = raw.toLowerCase();
|
|
151
|
-
if (lower ===
|
|
152
|
-
return
|
|
153
|
-
if (lower ===
|
|
154
|
-
return
|
|
155
|
-
if (lower ===
|
|
156
|
-
return
|
|
157
|
-
if (lower ===
|
|
158
|
-
return
|
|
159
|
-
if (lower ===
|
|
160
|
-
return
|
|
161
|
-
return
|
|
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
|
|
166
|
-
|
|
167
|
-
case
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
180
|
-
const resolved = baseline.findings.filter(f => !currentKeySet.has(makeFindingKey(f)));
|
|
181
|
-
|
|
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 ===
|
|
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,
|
|
328
|
+
report(onStep, "detect", "Detecting stack", "running");
|
|
237
329
|
let stackInfo;
|
|
238
330
|
try {
|
|
239
331
|
stackInfo = await detectCIStack(projectDir);
|
|
240
|
-
report(onStep,
|
|
332
|
+
report(onStep, "detect", `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, "done");
|
|
241
333
|
}
|
|
242
334
|
catch (e) {
|
|
243
|
-
report(onStep,
|
|
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,
|
|
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,
|
|
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,
|
|
349
|
+
report(onStep, "audit", `Audit complete`, "done");
|
|
258
350
|
}
|
|
259
351
|
catch (e) {
|
|
260
|
-
report(onStep,
|
|
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
|
|
267
|
-
case
|
|
268
|
-
report(onStep,
|
|
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
|
|
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,
|
|
382
|
+
report(onStep, "save", `Baseline saved with ${findings.length} finding(s)`, "done", baselinePath(projectDir));
|
|
279
383
|
return null;
|
|
280
384
|
}
|
|
281
|
-
case
|
|
282
|
-
report(onStep,
|
|
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,
|
|
286
|
-
throw new Error(
|
|
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
|
-
|
|
289
|
-
if (result.
|
|
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
|
-
|
|
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.
|
|
297
|
-
.map(r => ` ${r.severity.toUpperCase()} ${r.package}: ${r.title}`)
|
|
298
|
-
.join(
|
|
299
|
-
report(onStep,
|
|
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
|