@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/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
+ }
@@ -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
+ }