@hungpg/skill-audit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/SKILL.md +227 -0
- package/dist/audit.js +464 -0
- package/dist/deps.js +408 -0
- package/dist/discover.js +124 -0
- package/dist/index.js +195 -0
- package/dist/intel.js +416 -0
- package/dist/reporter.js +77 -0
- package/dist/scoring.js +129 -0
- package/dist/security.js +341 -0
- package/dist/spec.js +271 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { discoverSkills } from "./discover.js";
|
|
4
|
+
import { auditSecurity } from "./security.js";
|
|
5
|
+
import { validateSkillSpec } from "./spec.js";
|
|
6
|
+
import { createGroupedAuditResult } from "./scoring.js";
|
|
7
|
+
import { scanDependencies } from "./deps.js";
|
|
8
|
+
import { getKEV, getEPSS, isCacheStale } from "./intel.js";
|
|
9
|
+
import { writeFileSync } from "fs";
|
|
10
|
+
// Build CLI - no subcommands, just options + action
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("skills-audit")
|
|
14
|
+
.description("Security auditing CLI for AI agent skills")
|
|
15
|
+
.version("0.1.0")
|
|
16
|
+
.option("-g, --global", "Audit global skills only (default: true)")
|
|
17
|
+
.option("-p, --project", "Audit project-level skills only")
|
|
18
|
+
.option("-a, --agent <agents...>", "Filter by specific agents")
|
|
19
|
+
.option("-j, --json", "Output as JSON")
|
|
20
|
+
.option("-o, --output <file>", "Save report to file (JSON format)")
|
|
21
|
+
.option("-v, --verbose", "Show detailed findings")
|
|
22
|
+
.option("-t, --threshold <score>", "Fail if risk score exceeds threshold", parseFloat)
|
|
23
|
+
.option("--no-deps", "Skip dependency scanning (faster)")
|
|
24
|
+
.option("--mode <mode>", "Audit mode: 'lint' (spec only) or 'audit' (full)", "audit")
|
|
25
|
+
.option("--update-db", "Update advisory intelligence feeds")
|
|
26
|
+
.option("--source <sources...>", "Sources for update-db: kev, epss, all", ["all"])
|
|
27
|
+
.option("--strict", "Fail if feeds are stale")
|
|
28
|
+
.option("--quiet", "Suppress non-error output");
|
|
29
|
+
program.parse(process.argv);
|
|
30
|
+
const options = program.opts();
|
|
31
|
+
// Handle update-db action
|
|
32
|
+
if (options.updateDb) {
|
|
33
|
+
await updateAdvisoryDB({ source: options.source, strict: options.strict });
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
// Default to global skills
|
|
37
|
+
const scope = options.project ? "project" : "global";
|
|
38
|
+
const mode = options.mode || "audit";
|
|
39
|
+
if (!options.json) {
|
|
40
|
+
console.log(mode === "lint"
|
|
41
|
+
? "š Linting skills (spec validation)..."
|
|
42
|
+
: "š Auditing skills (full security + intelligence)...");
|
|
43
|
+
}
|
|
44
|
+
const skills = await discoverSkills(scope);
|
|
45
|
+
// Filter by agents if specified
|
|
46
|
+
let filteredSkills = skills;
|
|
47
|
+
if (options.agent && options.agent.length > 0) {
|
|
48
|
+
filteredSkills = skills.filter(s => s.agents.some(a => options.agent.includes(a)));
|
|
49
|
+
}
|
|
50
|
+
if (!options.json) {
|
|
51
|
+
console.log("Found " + filteredSkills.length + " skills\n");
|
|
52
|
+
}
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const skill of filteredSkills) {
|
|
55
|
+
// Step 1: Spec validation (always runs first)
|
|
56
|
+
const specResult = validateSkillSpec(skill.path, skill.name);
|
|
57
|
+
// Step 2: Security audit (full or lite based on mode)
|
|
58
|
+
let securityResult = { findings: [], unreadableFiles: [] };
|
|
59
|
+
let depFindings = [];
|
|
60
|
+
if (mode === "audit") {
|
|
61
|
+
securityResult = auditSecurity(skill, specResult.manifest);
|
|
62
|
+
if (options.deps !== false) {
|
|
63
|
+
depFindings = scanDependencies(skill.path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const allSecurityFindings = [...securityResult.findings, ...depFindings];
|
|
67
|
+
const result = createGroupedAuditResult(skill, specResult.manifest, specResult.findings, allSecurityFindings, []);
|
|
68
|
+
results.push(result);
|
|
69
|
+
}
|
|
70
|
+
reportGroupedResults(results, {
|
|
71
|
+
json: options.json,
|
|
72
|
+
output: options.output,
|
|
73
|
+
verbose: options.verbose,
|
|
74
|
+
threshold: options.threshold,
|
|
75
|
+
mode
|
|
76
|
+
});
|
|
77
|
+
async function updateAdvisoryDB(opts) {
|
|
78
|
+
const sources = opts.source.includes("all") ? ["kev", "epss"] : opts.source;
|
|
79
|
+
const quiet = program.opts().quiet;
|
|
80
|
+
if (!quiet) {
|
|
81
|
+
console.log("š„ Updating advisory intelligence feeds...\n");
|
|
82
|
+
}
|
|
83
|
+
let hasErrors = false;
|
|
84
|
+
for (const source of sources) {
|
|
85
|
+
if (!quiet) {
|
|
86
|
+
console.log(`Fetching ${source.toUpperCase()}...`);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
if (source === "kev") {
|
|
90
|
+
const result = await getKEV();
|
|
91
|
+
if (!quiet) {
|
|
92
|
+
console.log(` ā CISA KEV: ${result.findings.length} vulnerabilities cached (stale: ${result.stale})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (source === "epss") {
|
|
96
|
+
const result = await getEPSS();
|
|
97
|
+
if (!quiet) {
|
|
98
|
+
console.log(` ā EPSS: ${result.findings.length} scores cached (stale: ${result.stale})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error(` ā Failed to fetch ${source}:`, e);
|
|
104
|
+
hasErrors = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!quiet) {
|
|
108
|
+
console.log("\nā
Advisory DB updated");
|
|
109
|
+
}
|
|
110
|
+
if (opts.strict && hasErrors) {
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function reportGroupedResults(results, options) {
|
|
115
|
+
const { json, output, verbose, threshold, mode } = options;
|
|
116
|
+
// Export to file if specified
|
|
117
|
+
if (output) {
|
|
118
|
+
const report = {
|
|
119
|
+
generated: new Date().toISOString(),
|
|
120
|
+
mode,
|
|
121
|
+
summary: {
|
|
122
|
+
total: results.length,
|
|
123
|
+
safe: results.filter(r => r.riskLevel === "safe").length,
|
|
124
|
+
risky: results.filter(r => r.riskLevel === "risky").length,
|
|
125
|
+
dangerous: results.filter(r => r.riskLevel === "dangerous").length,
|
|
126
|
+
malicious: results.filter(r => r.riskLevel === "malicious").length,
|
|
127
|
+
specIssues: results.filter(r => r.specFindings.length > 0).length,
|
|
128
|
+
securityIssues: results.filter(r => r.securityFindings.length > 0).length
|
|
129
|
+
},
|
|
130
|
+
results
|
|
131
|
+
};
|
|
132
|
+
writeFileSync(output, JSON.stringify(report, null, 2));
|
|
133
|
+
console.log(`\nš Report saved to: ${output}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (json) {
|
|
137
|
+
console.log(JSON.stringify(results, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let safeCount = 0, riskyCount = 0, dangerousCount = 0, maliciousCount = 0;
|
|
141
|
+
let specErrors = 0, securityIssues = 0;
|
|
142
|
+
for (const r of results) {
|
|
143
|
+
if (r.riskLevel === "safe")
|
|
144
|
+
safeCount++;
|
|
145
|
+
else if (r.riskLevel === "risky")
|
|
146
|
+
riskyCount++;
|
|
147
|
+
else if (r.riskLevel === "dangerous")
|
|
148
|
+
dangerousCount++;
|
|
149
|
+
else
|
|
150
|
+
maliciousCount++;
|
|
151
|
+
if (r.specFindings.length > 0)
|
|
152
|
+
specErrors++;
|
|
153
|
+
if (r.securityFindings.length > 0)
|
|
154
|
+
securityIssues++;
|
|
155
|
+
}
|
|
156
|
+
console.log(`\nš Summary (${mode} mode):`);
|
|
157
|
+
console.log(` Safe: ${safeCount} | Risky: ${riskyCount} | Dangerous: ${dangerousCount} | Malicious: ${maliciousCount}`);
|
|
158
|
+
console.log(` Skills with spec issues: ${specErrors} | Security issues: ${securityIssues}`);
|
|
159
|
+
// Check cache freshness and warn if stale
|
|
160
|
+
const kevStale = isCacheStale("kev");
|
|
161
|
+
const epssStale = isCacheStale("epss");
|
|
162
|
+
if (!options.json && (kevStale.warn || epssStale.warn)) {
|
|
163
|
+
console.log(`\nā ļø Vulnerability DB is stale (${kevStale.age?.toFixed(1)} days for KEV, ${epssStale.age?.toFixed(1)} days for EPSS)`);
|
|
164
|
+
console.log(` Run: npx skill-audit --update-db`);
|
|
165
|
+
}
|
|
166
|
+
if (threshold !== undefined) {
|
|
167
|
+
const failing = results.filter(r => r.riskScore > threshold);
|
|
168
|
+
if (failing.length > 0) {
|
|
169
|
+
console.log(`\nā ${failing.length} skills exceed threshold ${threshold}`);
|
|
170
|
+
for (const f of failing) {
|
|
171
|
+
console.log(` - ${f.skill.name}: ${f.riskScore}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(`\nā
All skills pass threshold ${threshold}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (verbose) {
|
|
179
|
+
for (const r of results) {
|
|
180
|
+
console.log(`\n--- ${r.skill.name} ---`);
|
|
181
|
+
if (r.specFindings.length > 0) {
|
|
182
|
+
console.log(`\nš Spec Issues (${r.specFindings.length}):`);
|
|
183
|
+
for (const f of r.specFindings) {
|
|
184
|
+
console.log(` [${f.severity.toUpperCase()}] ${f.id}: ${f.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (r.securityFindings.length > 0) {
|
|
188
|
+
console.log(`\nš Security Issues (${r.securityFindings.length}):`);
|
|
189
|
+
for (const f of r.securityFindings) {
|
|
190
|
+
console.log(` [${f.severity.toUpperCase()}] ${f.id}: ${f.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
package/dist/intel.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
// Cache directory - in package root (parent of src)
|
|
5
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const CACHE_DIR = join(PACKAGE_ROOT, ".cache/skill-audit/feeds");
|
|
7
|
+
const METRICS_FILE = join(PACKAGE_ROOT, ".cache/skill-audit/metrics.json");
|
|
8
|
+
// Cache configuration - differentiated by source update frequency
|
|
9
|
+
const MAX_CACHE_AGE_DAYS = {
|
|
10
|
+
kev: 1, // Daily updates - critical for actively exploited vulns
|
|
11
|
+
epss: 3, // Matches FIRST.org update cycle
|
|
12
|
+
osv: 7 // Stable database - weekly acceptable
|
|
13
|
+
};
|
|
14
|
+
const WARN_CACHE_AGE_DAYS = 3;
|
|
15
|
+
const FETCH_TIMEOUT_MS = 30000; // 30 seconds
|
|
16
|
+
const MAX_RETRIES = 3;
|
|
17
|
+
const RETRY_DELAY_MS = 1000; // Base delay for exponential backoff
|
|
18
|
+
/**
|
|
19
|
+
* Ensure cache directory exists
|
|
20
|
+
*/
|
|
21
|
+
function ensureCacheDir() {
|
|
22
|
+
if (!existsSync(CACHE_DIR)) {
|
|
23
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
// Also ensure parent dir exists for metrics file
|
|
26
|
+
const metricsDir = dirname(METRICS_FILE);
|
|
27
|
+
if (!existsSync(metricsDir)) {
|
|
28
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fetch with retry and exponential backoff
|
|
33
|
+
*/
|
|
34
|
+
async function fetchWithRetry(url, timeoutMs = FETCH_TIMEOUT_MS, options = {}) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
37
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
...options,
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
headers: {
|
|
43
|
+
'User-Agent': 'skill-audit/0.1.0 (Vulnerability Intelligence Scanner)',
|
|
44
|
+
...options.headers
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
console.error(`Fetch failed (${url}): HTTP ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
56
|
+
throw error; // Last attempt - rethrow
|
|
57
|
+
}
|
|
58
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
59
|
+
const delay = RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
60
|
+
console.error(`Fetch failed (${url}), retrying in ${delay}ms... (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Max retries exceeded');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load update metrics
|
|
68
|
+
*/
|
|
69
|
+
function loadMetrics() {
|
|
70
|
+
if (!existsSync(METRICS_FILE)) {
|
|
71
|
+
return { lastUpdate: '', kevCount: 0, epssCount: 0, fetchDurationMs: 0, errors: [] };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(METRICS_FILE, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { lastUpdate: '', kevCount: 0, epssCount: 0, fetchDurationMs: 0, errors: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Save update metrics
|
|
82
|
+
*/
|
|
83
|
+
function saveMetrics(metrics) {
|
|
84
|
+
try {
|
|
85
|
+
writeFileSync(METRICS_FILE, JSON.stringify(metrics, null, 2));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('Failed to save metrics:', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Record fetch result in metrics
|
|
93
|
+
*/
|
|
94
|
+
function recordFetchResult(source, count, durationMs, error) {
|
|
95
|
+
const metrics = loadMetrics();
|
|
96
|
+
metrics.lastUpdate = new Date().toISOString();
|
|
97
|
+
metrics.fetchDurationMs += durationMs;
|
|
98
|
+
if (source === 'kev') {
|
|
99
|
+
metrics.kevCount = count;
|
|
100
|
+
}
|
|
101
|
+
else if (source === 'epss') {
|
|
102
|
+
metrics.epssCount = count;
|
|
103
|
+
}
|
|
104
|
+
if (error) {
|
|
105
|
+
metrics.errors.push(`${source}: ${error}`);
|
|
106
|
+
// Keep only last 10 errors
|
|
107
|
+
if (metrics.errors.length > 10) {
|
|
108
|
+
metrics.errors = metrics.errors.slice(-10);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
saveMetrics(metrics);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get cache file path for a source
|
|
115
|
+
*/
|
|
116
|
+
function getCachePath(source) {
|
|
117
|
+
ensureCacheDir();
|
|
118
|
+
return join(CACHE_DIR, `${source.toLowerCase()}.jsonl`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get metadata file path
|
|
122
|
+
*/
|
|
123
|
+
function getMetaPath(source) {
|
|
124
|
+
ensureCacheDir();
|
|
125
|
+
return join(CACHE_DIR, `${source.toLowerCase()}.meta.json`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if cache is stale and return age info
|
|
129
|
+
*/
|
|
130
|
+
export function isCacheStale(source) {
|
|
131
|
+
const metaPath = getMetaPath(source);
|
|
132
|
+
if (!existsSync(metaPath)) {
|
|
133
|
+
return { stale: true, warn: false };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
137
|
+
const fetchedAt = new Date(meta.fetchedAt);
|
|
138
|
+
const now = new Date();
|
|
139
|
+
const ageMs = now.getTime() - fetchedAt.getTime();
|
|
140
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
141
|
+
// Use source-specific max age
|
|
142
|
+
const maxAge = MAX_CACHE_AGE_DAYS[source.toLowerCase()] || MAX_CACHE_AGE_DAYS.osv;
|
|
143
|
+
return {
|
|
144
|
+
stale: ageDays > maxAge,
|
|
145
|
+
age: ageDays,
|
|
146
|
+
warn: ageDays > WARN_CACHE_AGE_DAYS
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return { stale: true, warn: false };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Save advisory records to cache
|
|
155
|
+
*/
|
|
156
|
+
function saveToCache(source, records) {
|
|
157
|
+
const cachePath = getCachePath(source);
|
|
158
|
+
const metaPath = getMetaPath(source);
|
|
159
|
+
// Save records as JSONL
|
|
160
|
+
const lines = records.map(r => JSON.stringify(r)).join('\n');
|
|
161
|
+
writeFileSync(cachePath, lines);
|
|
162
|
+
// Save metadata
|
|
163
|
+
const meta = {
|
|
164
|
+
source,
|
|
165
|
+
fetchedAt: new Date().toISOString(),
|
|
166
|
+
recordCount: records.length
|
|
167
|
+
};
|
|
168
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Load cached records
|
|
172
|
+
*/
|
|
173
|
+
function loadFromCache(source) {
|
|
174
|
+
const cachePath = getCachePath(source);
|
|
175
|
+
if (!existsSync(cachePath)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
180
|
+
return content.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Query OSV API for vulnerabilities (using native fetch)
|
|
188
|
+
* Note: Replaces curl-based approach with native Node.js HTTP
|
|
189
|
+
*/
|
|
190
|
+
export async function queryOSV(ecosystem, packageName) {
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetchWithRetry('https://api.osv.dev/v1/query', FETCH_TIMEOUT_MS, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
package: {
|
|
199
|
+
name: packageName,
|
|
200
|
+
ecosystem: ecosystem
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
console.error(`OSV API error: ${response.status}`);
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
if (!data.vulns) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
return data.vulns.map(v => ({
|
|
213
|
+
id: v.id,
|
|
214
|
+
aliases: v.aliases || [],
|
|
215
|
+
source: "OSV",
|
|
216
|
+
ecosystem,
|
|
217
|
+
packageName,
|
|
218
|
+
severity: v.severity?.[0]?.type,
|
|
219
|
+
published: v.published,
|
|
220
|
+
modified: v.modified,
|
|
221
|
+
summary: v.summary,
|
|
222
|
+
references: v.references?.map(r => r.url) || []
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
console.error(`OSV query failed for ${ecosystem}/${packageName}:`, error);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Query GHSA via GitHub API
|
|
232
|
+
*/
|
|
233
|
+
export async function queryGHSA(ecosystem, packageName) {
|
|
234
|
+
// GHSA API requires authentication for higher rate limits
|
|
235
|
+
// This is a placeholder - in production, use a GitHub token
|
|
236
|
+
try {
|
|
237
|
+
const ghsaEcosystemMap = {
|
|
238
|
+
'npm': 'npm',
|
|
239
|
+
'pypi': 'PyPI',
|
|
240
|
+
'go': 'Go',
|
|
241
|
+
'cargo': 'Cargo',
|
|
242
|
+
'rubygems': 'RubyGems',
|
|
243
|
+
'maven': 'Maven',
|
|
244
|
+
'nuget': 'NuGet'
|
|
245
|
+
};
|
|
246
|
+
const ghsaQuery = encodeURIComponent(`${packageName} repo:github/advisory-database`);
|
|
247
|
+
// Note: This is a simplified approach - production would use GraphQL API
|
|
248
|
+
console.log(`GHSA query: ${ecosystem}/${packageName} (API token recommended for production)`);
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`GHSA query failed:`, error);
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Fetch CISA KEV (Known Exploited Vulnerabilities)
|
|
258
|
+
*/
|
|
259
|
+
export async function fetchKEV() {
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
const url = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetchWithRetry(url);
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
if (!data.vulnerabilities) {
|
|
266
|
+
recordFetchResult('kev', 0, Date.now() - startTime, 'No vulnerabilities in response');
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const records = data.vulnerabilities.map(v => ({
|
|
270
|
+
id: v.cveID,
|
|
271
|
+
aliases: [v.cveID],
|
|
272
|
+
source: "KEV",
|
|
273
|
+
kev: true,
|
|
274
|
+
published: v.dateAdded,
|
|
275
|
+
summary: v.shortDescription,
|
|
276
|
+
references: v.reference ? [v.reference] : []
|
|
277
|
+
}));
|
|
278
|
+
recordFetchResult('kev', records.length, Date.now() - startTime);
|
|
279
|
+
return records;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
283
|
+
recordFetchResult('kev', 0, Date.now() - startTime, errorMsg);
|
|
284
|
+
console.error(`KEV fetch failed:`, error);
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Fetch EPSS scores
|
|
290
|
+
*/
|
|
291
|
+
export async function fetchEPSS() {
|
|
292
|
+
const startTime = Date.now();
|
|
293
|
+
const url = 'https://api.first.org/data/v1/epss?limit=500&sort=epss';
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetchWithRetry(url);
|
|
296
|
+
const data = await response.json();
|
|
297
|
+
if (!data.data) {
|
|
298
|
+
recordFetchResult('epss', 0, Date.now() - startTime, 'No data in response');
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const records = data.data.map(entry => ({
|
|
302
|
+
id: entry.cve,
|
|
303
|
+
aliases: [entry.cve],
|
|
304
|
+
source: "EPSS",
|
|
305
|
+
epss: parseFloat(entry.epss),
|
|
306
|
+
published: entry.date,
|
|
307
|
+
references: []
|
|
308
|
+
}));
|
|
309
|
+
recordFetchResult('epss', records.length, Date.now() - startTime);
|
|
310
|
+
return records;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
314
|
+
recordFetchResult('epss', 0, Date.now() - startTime, errorMsg);
|
|
315
|
+
console.error(`EPSS fetch failed:`, error);
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Query vulnerability intelligence for a package
|
|
321
|
+
*/
|
|
322
|
+
export async function queryIntel(ecosystem, packageName) {
|
|
323
|
+
const findings = [];
|
|
324
|
+
// Check cache freshness
|
|
325
|
+
const { stale, age } = isCacheStale("osv");
|
|
326
|
+
// Try OSV first (most comprehensive for package vulns)
|
|
327
|
+
const osvResults = await queryOSV(ecosystem, packageName);
|
|
328
|
+
findings.push(...osvResults);
|
|
329
|
+
// Try GHSA
|
|
330
|
+
const ghsaResults = await queryGHSA(ecosystem, packageName);
|
|
331
|
+
findings.push(...ghsaResults);
|
|
332
|
+
return {
|
|
333
|
+
findings,
|
|
334
|
+
cacheAge: age,
|
|
335
|
+
stale
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get KEV vulnerabilities (enriched)
|
|
340
|
+
*/
|
|
341
|
+
export async function getKEV() {
|
|
342
|
+
const { stale, age, warn } = isCacheStale("kev");
|
|
343
|
+
let records = loadFromCache("kev");
|
|
344
|
+
if (records.length === 0 || stale) {
|
|
345
|
+
records = await fetchKEV();
|
|
346
|
+
if (records.length > 0) {
|
|
347
|
+
saveToCache("kev", records);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
findings: records,
|
|
352
|
+
cacheAge: age,
|
|
353
|
+
stale,
|
|
354
|
+
warn
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get EPSS scores (enriched)
|
|
359
|
+
*/
|
|
360
|
+
export async function getEPSS() {
|
|
361
|
+
const { stale, age, warn } = isCacheStale("epss");
|
|
362
|
+
let records = loadFromCache("epss");
|
|
363
|
+
if (records.length === 0 || stale) {
|
|
364
|
+
records = await fetchEPSS();
|
|
365
|
+
if (records.length > 0) {
|
|
366
|
+
saveToCache("epss", records);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
findings: records,
|
|
371
|
+
cacheAge: age,
|
|
372
|
+
stale,
|
|
373
|
+
warn
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Merge advisory records by alias
|
|
378
|
+
*/
|
|
379
|
+
export function mergeByAlias(records) {
|
|
380
|
+
const aliasMap = new Map();
|
|
381
|
+
for (const record of records) {
|
|
382
|
+
// Use all IDs and aliases as keys
|
|
383
|
+
const ids = [record.id, ...record.aliases];
|
|
384
|
+
for (const id of ids) {
|
|
385
|
+
const key = id.toUpperCase();
|
|
386
|
+
if (!aliasMap.has(key)) {
|
|
387
|
+
aliasMap.set(key, []);
|
|
388
|
+
}
|
|
389
|
+
aliasMap.get(key).push(record);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return aliasMap;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Prioritize records by source (OSV/GHSA > KEV > EPSS)
|
|
396
|
+
*/
|
|
397
|
+
export function prioritizeRecords(records) {
|
|
398
|
+
const sourcePriority = {
|
|
399
|
+
"OSV": 1,
|
|
400
|
+
"GHSA": 2,
|
|
401
|
+
"NVD": 3,
|
|
402
|
+
"KEV": 4,
|
|
403
|
+
"EPSS": 5
|
|
404
|
+
};
|
|
405
|
+
return [...records].sort((a, b) => {
|
|
406
|
+
const aP = sourcePriority[a.source] || 10;
|
|
407
|
+
const bP = sourcePriority[b.source] || 10;
|
|
408
|
+
if (aP !== bP)
|
|
409
|
+
return aP - bP;
|
|
410
|
+
// Secondary: EPSS score (higher is worse)
|
|
411
|
+
if (a.epss !== undefined && b.epss !== undefined) {
|
|
412
|
+
return b.epss - a.epss;
|
|
413
|
+
}
|
|
414
|
+
return 0;
|
|
415
|
+
});
|
|
416
|
+
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const LEVEL_ICONS = {
|
|
2
|
+
"safe": "ā
",
|
|
3
|
+
"risky": "ā ļø",
|
|
4
|
+
"dangerous": "š“",
|
|
5
|
+
"malicious": "ā ļø",
|
|
6
|
+
};
|
|
7
|
+
const SEVERITY_COLORS = {
|
|
8
|
+
"critical": "\x1b[31m",
|
|
9
|
+
"high": "\x1b[33m",
|
|
10
|
+
"medium": "\x1b[35m",
|
|
11
|
+
"low": "\x1b[36m",
|
|
12
|
+
"info": "\x1b[90m",
|
|
13
|
+
};
|
|
14
|
+
const RESET = "\x1b[0m";
|
|
15
|
+
function padEnd(str, len) {
|
|
16
|
+
while (str.length < len)
|
|
17
|
+
str += " ";
|
|
18
|
+
return str;
|
|
19
|
+
}
|
|
20
|
+
export function reportResults(results, options) {
|
|
21
|
+
if (options.json) {
|
|
22
|
+
console.log(JSON.stringify(results, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const byAgent = new Map();
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
for (const agent of result.skill.agents) {
|
|
28
|
+
if (!byAgent.has(agent))
|
|
29
|
+
byAgent.set(agent, []);
|
|
30
|
+
byAgent.get(agent).push(result);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const uniqueSkills = new Map();
|
|
34
|
+
for (const result of results) {
|
|
35
|
+
uniqueSkills.set(result.skill.name, result);
|
|
36
|
+
}
|
|
37
|
+
const uniqueResults = Array.from(uniqueSkills.values());
|
|
38
|
+
console.log("\nš Auditing installed skills...\n");
|
|
39
|
+
console.log("Total: " + uniqueResults.length + " skills scanned");
|
|
40
|
+
const safe = uniqueResults.filter(r => r.riskLevel === "safe").length;
|
|
41
|
+
const risky = uniqueResults.filter(r => r.riskLevel === "risky").length;
|
|
42
|
+
const dangerous = uniqueResults.filter(r => r.riskLevel === "dangerous").length;
|
|
43
|
+
const malicious = uniqueResults.filter(r => r.riskLevel === "malicious").length;
|
|
44
|
+
console.log("ā
Safe: " + safe + " | ā ļø Risky: " + risky + " | š“ Dangerous: " + dangerous + " | ā ļø Malicious: " + malicious + "\n");
|
|
45
|
+
for (const [agent, agentResults] of byAgent) {
|
|
46
|
+
const uniqueAgentSkills = new Map();
|
|
47
|
+
for (const r of agentResults) {
|
|
48
|
+
uniqueAgentSkills.set(r.skill.name, r);
|
|
49
|
+
}
|
|
50
|
+
console.log("š " + agent + " (" + uniqueAgentSkills.size + " skills)");
|
|
51
|
+
for (const result of uniqueAgentSkills.values()) {
|
|
52
|
+
const icon = LEVEL_ICONS[result.riskLevel];
|
|
53
|
+
const score = result.riskScore.toFixed(1);
|
|
54
|
+
console.log(" " + icon + " " + padEnd(result.skill.name, 40) + " " + score);
|
|
55
|
+
if (options.verbose && result.findings.length > 0) {
|
|
56
|
+
for (const f of result.findings.slice(0, 5)) {
|
|
57
|
+
const color = SEVERITY_COLORS[f.severity] || "";
|
|
58
|
+
console.log(" " + color + f.severity.toUpperCase() + RESET + ": [" + f.asixx + "] " + f.message);
|
|
59
|
+
}
|
|
60
|
+
if (result.findings.length > 5) {
|
|
61
|
+
console.log(" ... and " + (result.findings.length - 5) + " more");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log("");
|
|
66
|
+
}
|
|
67
|
+
if (options.threshold !== undefined) {
|
|
68
|
+
const failed = uniqueResults.filter(r => r.riskScore > options.threshold);
|
|
69
|
+
if (failed.length > 0) {
|
|
70
|
+
console.log("ā " + failed.length + " skills exceed threshold (" + options.threshold + ")");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log("ā
All skills within acceptable risk threshold");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|