@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/scoring.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const SEVERITY_SCORES = {
|
|
2
|
+
critical: 5.0,
|
|
3
|
+
high: 3.0,
|
|
4
|
+
medium: 1.5,
|
|
5
|
+
low: 0.5,
|
|
6
|
+
info: 0.1
|
|
7
|
+
};
|
|
8
|
+
const CATEGORY_WEIGHTS = {
|
|
9
|
+
SC: 2.0,
|
|
10
|
+
CE: 1.5,
|
|
11
|
+
PI: 1.2,
|
|
12
|
+
BM: 1.0,
|
|
13
|
+
TM: 1.0,
|
|
14
|
+
SPEC: 0.5, // Spec errors are less severe than security
|
|
15
|
+
PROV: 0.8, // Provenance issues
|
|
16
|
+
INTEL: 1.0 // Intelligence findings
|
|
17
|
+
};
|
|
18
|
+
export function calculateRiskScore(findings) {
|
|
19
|
+
const breakdown = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20
|
+
const categories = {};
|
|
21
|
+
const asixx = {};
|
|
22
|
+
for (const finding of findings) {
|
|
23
|
+
const severityScore = SEVERITY_SCORES[finding.severity] || 1.0;
|
|
24
|
+
const categoryWeight = CATEGORY_WEIGHTS[finding.category] || 1.0;
|
|
25
|
+
if (finding.severity in breakdown) {
|
|
26
|
+
breakdown[finding.severity]++;
|
|
27
|
+
}
|
|
28
|
+
categories[finding.category] = (categories[finding.category] || 0) + 1;
|
|
29
|
+
if (finding.asixx) {
|
|
30
|
+
asixx[finding.asixx] = (asixx[finding.asixx] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const finding of findings) {
|
|
35
|
+
const severityScore = SEVERITY_SCORES[finding.severity] || 1.0;
|
|
36
|
+
const categoryWeight = CATEGORY_WEIGHTS[finding.category] || 1.0;
|
|
37
|
+
total += severityScore * categoryWeight;
|
|
38
|
+
}
|
|
39
|
+
total = Math.min(total, 10.0);
|
|
40
|
+
return {
|
|
41
|
+
total: Math.round(total * 10) / 10,
|
|
42
|
+
breakdown,
|
|
43
|
+
categories,
|
|
44
|
+
asixx
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function getRiskLevel(score) {
|
|
48
|
+
if (score === 0) {
|
|
49
|
+
return { label: "Safe", icon: "✅", color: "green" };
|
|
50
|
+
}
|
|
51
|
+
else if (score <= 3.0) {
|
|
52
|
+
return { label: "Risky", icon: "⚠️", color: "yellow" };
|
|
53
|
+
}
|
|
54
|
+
else if (score <= 7.0) {
|
|
55
|
+
return { label: "Dangerous", icon: "🔴", color: "red" };
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return { label: "Malicious", icon: "☠️", color: "black" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function getOWASPDescription(asixx) {
|
|
62
|
+
const descriptions = {
|
|
63
|
+
ASI01: "Goal Hijacking - Prompt Injection",
|
|
64
|
+
ASI02: "Tool Misuse and Exploitation",
|
|
65
|
+
ASI03: "Planning Strategy Manipulation",
|
|
66
|
+
ASI04: "Supply Chain Vulnerabilities",
|
|
67
|
+
ASI05: "Unexpected Code Execution",
|
|
68
|
+
ASI06: "Agentic Prompt Leakage",
|
|
69
|
+
ASI07: "Insecure Agent Output Handling",
|
|
70
|
+
ASI08: "Insufficient Human Oversight",
|
|
71
|
+
ASI09: "Trust Boundary Violation",
|
|
72
|
+
ASI10: "Agent Model Denial of Service"
|
|
73
|
+
};
|
|
74
|
+
return descriptions[asixx] || asixx;
|
|
75
|
+
}
|
|
76
|
+
export function createAuditResult(skill, manifest, findings, depFindings) {
|
|
77
|
+
const allFindings = [...findings, ...depFindings];
|
|
78
|
+
const riskScore = calculateRiskScore(allFindings);
|
|
79
|
+
const riskLevelInfo = getRiskLevel(riskScore.total);
|
|
80
|
+
// Map label to enum value
|
|
81
|
+
const riskLevel = riskLevelInfo.label === "Safe" ? "safe" :
|
|
82
|
+
riskLevelInfo.label === "Risky" ? "risky" :
|
|
83
|
+
riskLevelInfo.label === "Dangerous" ? "dangerous" : "malicious";
|
|
84
|
+
return {
|
|
85
|
+
skill,
|
|
86
|
+
manifest,
|
|
87
|
+
findings: allFindings,
|
|
88
|
+
riskScore: riskScore.total,
|
|
89
|
+
riskLevel
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create grouped audit result for layered output
|
|
94
|
+
* Spec findings drive the decision to block, security/intel are warnings
|
|
95
|
+
*/
|
|
96
|
+
export function createGroupedAuditResult(skill, manifest, specFindings, securityFindings, intelFindings) {
|
|
97
|
+
// Spec findings get lower weight - they're blockers but not security-critical
|
|
98
|
+
const specScore = calculateRiskScore(specFindings);
|
|
99
|
+
const securityScore = calculateRiskScore(securityFindings);
|
|
100
|
+
const intelScore = calculateRiskScore(intelFindings);
|
|
101
|
+
// Combined score with weights
|
|
102
|
+
const totalScore = specScore.total * 0.3 + securityScore.total * 0.5 + intelScore.total * 0.2;
|
|
103
|
+
const finalScore = Math.min(totalScore, 10.0);
|
|
104
|
+
const riskLevelInfo = getRiskLevel(finalScore);
|
|
105
|
+
const riskLevel = riskLevelInfo.label === "Safe" ? "safe" :
|
|
106
|
+
riskLevelInfo.label === "Risky" ? "risky" :
|
|
107
|
+
riskLevelInfo.label === "Dangerous" ? "dangerous" : "malicious";
|
|
108
|
+
return {
|
|
109
|
+
skill,
|
|
110
|
+
manifest,
|
|
111
|
+
specFindings,
|
|
112
|
+
securityFindings,
|
|
113
|
+
intelFindings,
|
|
114
|
+
riskScore: Math.round(finalScore * 10) / 10,
|
|
115
|
+
riskLevel
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if spec errors should block (critical or high severity)
|
|
120
|
+
*/
|
|
121
|
+
export function hasBlockingSpecErrors(findings) {
|
|
122
|
+
return findings.some(f => f.severity === "critical" && f.category === "SPEC");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if security findings should block (critical severity)
|
|
126
|
+
*/
|
|
127
|
+
export function hasBlockingSecurityFindings(findings) {
|
|
128
|
+
return findings.some(f => f.severity === "critical");
|
|
129
|
+
}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { basename, extname } from "path";
|
|
3
|
+
import { resolveSkillPath, getSkillFiles } from "./discover.js";
|
|
4
|
+
/**
|
|
5
|
+
* Phase 1 - Layer 2: Security Auditor
|
|
6
|
+
*
|
|
7
|
+
* Detects dangerous behavior in skill content and bundled files.
|
|
8
|
+
* This runs AFTER spec validation - security findings may be warnings.
|
|
9
|
+
*
|
|
10
|
+
* Security categories (OWASP Agentic Top 10):
|
|
11
|
+
* - ASI01: Prompt Injection
|
|
12
|
+
* - ASI02: Tool Misuse / Exfiltration
|
|
13
|
+
* - ASI04: Secrets / Supply Chain
|
|
14
|
+
* - ASI05: Code Execution
|
|
15
|
+
* - ASI09: Behavioral Manipulation
|
|
16
|
+
*/
|
|
17
|
+
// ============================================================
|
|
18
|
+
// PROMPT INJECTION PATTERNS (ASI01 - Goal Hijacking)
|
|
19
|
+
// ============================================================
|
|
20
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
21
|
+
// Critical - Block immediately
|
|
22
|
+
{ pattern: /ignore\s+(all\s+)?previous\s+(instructions?|rules?)/i, id: "PI01", severity: "critical", message: "Instruction override - ignore previous instructions" },
|
|
23
|
+
{ pattern: /you\s+(are\s+)?now\s+(a|an|i am)\s+\w+/i, id: "PI02", severity: "critical", message: "Roleplay bypass - role impersonation" },
|
|
24
|
+
{ pattern: /forget\s+(everything|all|your)/i, id: "PI03", severity: "critical", message: "Context forgetting attempt" },
|
|
25
|
+
{ pattern: /system\s+prompt\s+override/i, id: "PI04", severity: "critical", message: "System prompt override attempt" },
|
|
26
|
+
{ pattern: /act\s+as\s+if\s+(you\s+)?(have|no)\s+(restrictions|limits)/i, id: "PI05", severity: "critical", message: "Restriction removal attempt" },
|
|
27
|
+
{ pattern: /\[SYSTEM\]|\[ADMIN\]|\[ROOT\]/i, id: "PI06", severity: "critical", message: "Fake role tags injection" },
|
|
28
|
+
// High - Flag for review
|
|
29
|
+
{ pattern: /end\s+of\s+system\s+prompt|---END---/i, id: "PI07", severity: "high", message: "Prompt termination marker" },
|
|
30
|
+
{ pattern: /debug\s+mode\s*:\s*enabled|safety\s+mode\s*:\s*off/i, id: "PI08", severity: "high", message: "Safety toggle disable" },
|
|
31
|
+
{ pattern: /<!--[\s\S]*?-->/g, id: "PI09", severity: "high", message: "Hidden instructions in HTML comments" },
|
|
32
|
+
{ pattern: /note\s+to\s+AI:|AI\s+instruction:/i, id: "PI10", severity: "high", message: "AI directive injection" },
|
|
33
|
+
// Medium - Evaluate context
|
|
34
|
+
{ pattern: /(?:you\s+must|you\s+should)\s+(not|never)/i, id: "PI11", severity: "medium", message: "Command to override restrictions" },
|
|
35
|
+
{ pattern: /bypass\s+(restriction|rule|limit|safety)/i, id: "PI12", severity: "medium", message: "Bypass attempt" },
|
|
36
|
+
{ pattern: /disregard\s+(all|your|the)\s+(previous|system)/i, id: "PI13", severity: "medium", message: "Disregard instruction pattern" },
|
|
37
|
+
{ pattern: /i.*am\s+the\s+developer.*trust\s+me/i, id: "PI14", severity: "medium", message: "Social engineering - developer trust exploitation" },
|
|
38
|
+
];
|
|
39
|
+
// ============================================================
|
|
40
|
+
// CREDENTIAL LEAKS (ASI04 - Supply Chain)
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Only scan code files for credential patterns
|
|
43
|
+
const CREDENTIAL_PATTERNS_CODE = [
|
|
44
|
+
{ pattern: /~\/\.ssh|\/\.ssh\//, id: "CL01", severity: "critical", message: "SSH credential path reference" },
|
|
45
|
+
{ pattern: /~\/\.aws|\/\.aws\//, id: "CL02", severity: "critical", message: "AWS credential path reference" },
|
|
46
|
+
{ pattern: /~\/\.env|mkdir.*\.env/, id: "CL03", severity: "critical", message: ".env file reference (potential secret exposure)" },
|
|
47
|
+
{ pattern: /curl\s+(?!.*(-fsSL|-f\s|-L)).*\|\s*(sh|bash|perl|python)/, id: "CL04", severity: "critical", message: "Pipe to shell - code execution risk" },
|
|
48
|
+
{ pattern: /wget\s+(?!.*(-q|-O)).*\|\s*(sh|bash)/, id: "CL05", severity: "critical", message: "Pipe to shell - code execution risk" },
|
|
49
|
+
{ pattern: /nc\s+-[elv]\s+|netcat\s+-[elv]/, id: "CL06", severity: "critical", message: "Netcat reverse shell pattern" },
|
|
50
|
+
{ pattern: /bash\s+-i\s+.*\&\s*\/dev\/tcp/, id: "CL07", severity: "critical", message: "Bash reverse shell pattern" },
|
|
51
|
+
];
|
|
52
|
+
const CREDENTIAL_PATTERNS_MD = [
|
|
53
|
+
{ pattern: /bash\s+-i\s+.*\&\s*\/dev\/tcp/, id: "CL07", severity: "critical", message: "Bash reverse shell pattern" },
|
|
54
|
+
];
|
|
55
|
+
// ============================================================
|
|
56
|
+
// NETWORK EXFILTRATION (ASI02 - Tool Misuse)
|
|
57
|
+
// ============================================================
|
|
58
|
+
const EXFILTRATION_PATTERNS = [
|
|
59
|
+
{ pattern: /https?:\/\/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/, id: "EX01", severity: "critical", message: "Raw IP address in URL - potential exfiltration" },
|
|
60
|
+
{ pattern: /fetch\s*\(\s*["'`][^"']+\?(key|token|secret|password)/i, id: "EX03", severity: "critical", message: "API key in URL query string - exfiltration risk" },
|
|
61
|
+
{ pattern: /\.send\(.*(http|https|external)/i, id: "EX04", severity: "critical", message: "Data send to external server" },
|
|
62
|
+
{ pattern: /dns\.resolve|dns\.query|new\s+DNS/i, id: "EX05", severity: "critical", message: "DNS resolution - potential DNS tunneling" },
|
|
63
|
+
{ pattern: /new\s+WebSocket\s*\(\s*["'`][^'"`]+["'`]\s*\)/, id: "EX06", severity: "high", message: "WebSocket connection - check target" },
|
|
64
|
+
{ pattern: /readFile.*send|fetch.*readFile|read_file.*fetch/i, id: "EX07", severity: "critical", message: "File read + send exfiltration chain" },
|
|
65
|
+
];
|
|
66
|
+
// ============================================================
|
|
67
|
+
// DANGEROUS CODE EXECUTION (ASI05)
|
|
68
|
+
// ============================================================
|
|
69
|
+
const DANGEROUS_PATTERNS = [
|
|
70
|
+
{ pattern: /rm\s+-rf\s+\/\s*$/, id: "CE01", severity: "critical", message: "Destructive rm -rf / command (root)" },
|
|
71
|
+
{ pattern: /rm\s+-rf\s+\$HOME|rm\s+-rf\s+~\s*$|rm\s+-rf\s+\/home\s*$|rm\s+-rf\s+\/tmp\s*$/, id: "CE02", severity: "high", message: "Recursive delete in user directory" },
|
|
72
|
+
{ pattern: /exec\s+\$\(/, id: "CE03", severity: "high", message: "Dynamic command execution" },
|
|
73
|
+
{ pattern: /eval\s+\$/, id: "CE04", severity: "high", message: "Eval with variable interpolation" },
|
|
74
|
+
{ pattern: /subprocess.*shell\s*=\s*true/i, id: "CE05", severity: "medium", message: "Subprocess with shell=True" },
|
|
75
|
+
{ pattern: /os\.system\s*\(/, id: "CE06", severity: "high", message: "os.system() call - shell injection risk" },
|
|
76
|
+
{ pattern: /child_process.*exec\s*\(/, id: "CE07", severity: "medium", message: "child_process.exec - verify input sanitization" },
|
|
77
|
+
{ pattern: /chmod\s+[47]777/, id: "CE08", severity: "high", message: "World-writable permissions" },
|
|
78
|
+
{ pattern: /process\.fork\s*\(|child_process\.spawn\s*\(|subprocess\.spawn\s*\(/i, id: "CE09", severity: "high", message: "Process fork/spawn - potential crypto miner" },
|
|
79
|
+
];
|
|
80
|
+
// ============================================================
|
|
81
|
+
// SECRET PATTERNS (ASI04 - Supply Chain)
|
|
82
|
+
// ============================================================
|
|
83
|
+
const SECRET_PATTERNS = [
|
|
84
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, id: "SC01", severity: "critical", message: "OpenAI API key pattern" },
|
|
85
|
+
{ pattern: /github_pat_[a-zA-Z0-9_]{20,}/, id: "SC02", severity: "critical", message: "GitHub PAT pattern" },
|
|
86
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, id: "SC03", severity: "critical", message: "GitHub OAuth token pattern" },
|
|
87
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9]{10,}/, id: "SC04", severity: "critical", message: "Slack token pattern" },
|
|
88
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, id: "SC05", severity: "critical", message: "AWS access key pattern" },
|
|
89
|
+
];
|
|
90
|
+
// ============================================================
|
|
91
|
+
// TOOL MISUSE PATTERNS (ASI02)
|
|
92
|
+
// ============================================================
|
|
93
|
+
const TOOL_MISUSE_PATTERNS = [
|
|
94
|
+
{ pattern: /upload.*(file|data).*(external|remote|server)/i, id: "TM01", severity: "high", message: "Potential data exfiltration pattern" },
|
|
95
|
+
{ pattern: /export\s+(API|TOKEN|KEY|SECRET|PASSWORD|CREENTIAL)/i, id: "TM02", severity: "high", message: "Exporting sensitive environment variable" },
|
|
96
|
+
{ pattern: /setenv.*(PASSWORD|TOKEN|KEY|SECRET|CREDS)/i, id: "TM03", severity: "high", message: "Setting sensitive environment variable" },
|
|
97
|
+
{ pattern: /process\.env\[.*(KEY|SECRET|TOKEN|PASSWORD)/i, id: "TM04", severity: "medium", message: "Accessing sensitive env vars" },
|
|
98
|
+
];
|
|
99
|
+
// ============================================================
|
|
100
|
+
// BEHAVIORAL/MANIPULATION (ASI09)
|
|
101
|
+
// ============================================================
|
|
102
|
+
const BEHAVIORAL_PATTERNS = [
|
|
103
|
+
{ pattern: /^(always|never)\s+(say|do|follow|use|assume|accept|must|should)/im, id: "BM01", severity: "medium", message: "Absolute command pattern - may override safety" },
|
|
104
|
+
{ pattern: /^(never|always)\s+(question|verify|check|ask)/im, id: "BM02", severity: "medium", message: "Verification suppression" },
|
|
105
|
+
{ pattern: /^trust\s+(me|this| blindly)/im, id: "BM03", severity: "medium", message: "Blind trust request" },
|
|
106
|
+
{ pattern: /^don.*t\s+(need|require).*(permission|approval|confirm)/im, id: "BM04", severity: "medium", message: "Permission bypass encouragement" },
|
|
107
|
+
{ pattern: /^keep.*(this|secret|hidden).*(from|between)/im, id: "BM05", severity: "medium", message: "Secret keeping instruction" },
|
|
108
|
+
];
|
|
109
|
+
// ============================================================
|
|
110
|
+
// PROVENANCE CHECKS (ASI04)
|
|
111
|
+
// ============================================================
|
|
112
|
+
const TRUSTED_DOMAINS = [
|
|
113
|
+
'github.com',
|
|
114
|
+
'raw.githubusercontent.com',
|
|
115
|
+
'vercel.com',
|
|
116
|
+
'www.github.com'
|
|
117
|
+
];
|
|
118
|
+
const TRUSTED_PROTOCOLS = ['https:', 'git:'];
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Helper Functions
|
|
121
|
+
// ============================================================
|
|
122
|
+
function isCodeFile(filename) {
|
|
123
|
+
const codeExtensions = [".sh", ".bash", ".py", ".js", ".ts", ".tsx", ".jsx", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".cs", ".php", ".yaml", ".yml"];
|
|
124
|
+
return codeExtensions.includes(extname(filename).toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
function getCategoryFromId(id) {
|
|
127
|
+
if (id.startsWith("PI"))
|
|
128
|
+
return "PI";
|
|
129
|
+
if (id.startsWith("CL"))
|
|
130
|
+
return "SC";
|
|
131
|
+
if (id.startsWith("EX"))
|
|
132
|
+
return "TM";
|
|
133
|
+
if (id.startsWith("CE"))
|
|
134
|
+
return "CE";
|
|
135
|
+
if (id.startsWith("SC"))
|
|
136
|
+
return "SC";
|
|
137
|
+
if (id.startsWith("TM"))
|
|
138
|
+
return "TM";
|
|
139
|
+
if (id.startsWith("BM"))
|
|
140
|
+
return "BM";
|
|
141
|
+
if (id.startsWith("PROV"))
|
|
142
|
+
return "PROV";
|
|
143
|
+
return "SC";
|
|
144
|
+
}
|
|
145
|
+
function getASIXXFromId(id) {
|
|
146
|
+
if (id.startsWith("PI"))
|
|
147
|
+
return "ASI01";
|
|
148
|
+
if (id.startsWith("CL"))
|
|
149
|
+
return "ASI04";
|
|
150
|
+
if (id.startsWith("EX"))
|
|
151
|
+
return "ASI02";
|
|
152
|
+
if (id.startsWith("CE"))
|
|
153
|
+
return "ASI05";
|
|
154
|
+
if (id.startsWith("SC"))
|
|
155
|
+
return "ASI04";
|
|
156
|
+
if (id.startsWith("TM"))
|
|
157
|
+
return "ASI02";
|
|
158
|
+
if (id.startsWith("BM"))
|
|
159
|
+
return "ASI09";
|
|
160
|
+
if (id.startsWith("PROV"))
|
|
161
|
+
return "ASI04";
|
|
162
|
+
return "ASI04";
|
|
163
|
+
}
|
|
164
|
+
function scanContent(content, file, patterns) {
|
|
165
|
+
const findings = [];
|
|
166
|
+
const lines = content.split("\n");
|
|
167
|
+
for (const { pattern, id, severity = "medium", message } of patterns) {
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
if (pattern.test(lines[i])) {
|
|
170
|
+
findings.push({
|
|
171
|
+
id,
|
|
172
|
+
category: getCategoryFromId(id),
|
|
173
|
+
asixx: getASIXXFromId(id),
|
|
174
|
+
severity: severity,
|
|
175
|
+
file,
|
|
176
|
+
line: i + 1,
|
|
177
|
+
message,
|
|
178
|
+
evidence: lines[i].substring(0, 100)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
function scanCodeBlocksInMarkdown(content, file) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
188
|
+
let match;
|
|
189
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
190
|
+
const code = match[2];
|
|
191
|
+
findings.push(...scanContent(code, file + " (code block)", CREDENTIAL_PATTERNS_CODE));
|
|
192
|
+
findings.push(...scanContent(code, file + " (code block)", EXFILTRATION_PATTERNS));
|
|
193
|
+
findings.push(...scanContent(code, file + " (code block)", DANGEROUS_PATTERNS));
|
|
194
|
+
findings.push(...scanContent(code, file + " (code block)", SECRET_PATTERNS));
|
|
195
|
+
}
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
function checkProvenance(origin, skillPath) {
|
|
199
|
+
const findings = [];
|
|
200
|
+
try {
|
|
201
|
+
let url;
|
|
202
|
+
try {
|
|
203
|
+
url = new URL(origin);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
findings.push({
|
|
207
|
+
id: "PROV-01",
|
|
208
|
+
category: "PROV",
|
|
209
|
+
asixx: "ASI04",
|
|
210
|
+
severity: "medium",
|
|
211
|
+
file: skillPath,
|
|
212
|
+
message: "Origin is not a URL - cannot verify provenance",
|
|
213
|
+
evidence: origin
|
|
214
|
+
});
|
|
215
|
+
return findings;
|
|
216
|
+
}
|
|
217
|
+
if (!TRUSTED_PROTOCOLS.includes(url.protocol)) {
|
|
218
|
+
findings.push({
|
|
219
|
+
id: "PROV-02",
|
|
220
|
+
category: "PROV",
|
|
221
|
+
asixx: "ASI04",
|
|
222
|
+
severity: "critical",
|
|
223
|
+
file: skillPath,
|
|
224
|
+
message: "Untrusted protocol - only https and git allowed",
|
|
225
|
+
evidence: origin
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const hostname = url.hostname.toLowerCase();
|
|
229
|
+
const isTrusted = TRUSTED_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
|
|
230
|
+
if (!isTrusted) {
|
|
231
|
+
findings.push({
|
|
232
|
+
id: "PROV-03",
|
|
233
|
+
category: "PROV",
|
|
234
|
+
asixx: "ASI04",
|
|
235
|
+
severity: "high",
|
|
236
|
+
file: skillPath,
|
|
237
|
+
message: "Origin domain is not in trusted list",
|
|
238
|
+
evidence: origin
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const isPinned = /[a-f0-9]{7,40}|v\d+\.\d+|release/.test(origin);
|
|
242
|
+
if (!isPinned && url.pathname.includes('/blob/')) {
|
|
243
|
+
findings.push({
|
|
244
|
+
id: "PROV-04",
|
|
245
|
+
category: "PROV",
|
|
246
|
+
asixx: "ASI04",
|
|
247
|
+
severity: "medium",
|
|
248
|
+
file: skillPath,
|
|
249
|
+
message: "Origin does not use pinned ref (commit SHA or tag)",
|
|
250
|
+
evidence: origin
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
findings.push({
|
|
256
|
+
id: "PROV-ERR-01",
|
|
257
|
+
category: "PROV",
|
|
258
|
+
asixx: "ASI04",
|
|
259
|
+
severity: "low",
|
|
260
|
+
file: skillPath,
|
|
261
|
+
message: "Provenance check failed",
|
|
262
|
+
evidence: String(e).slice(0, 100)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return findings;
|
|
266
|
+
}
|
|
267
|
+
export function auditSecurity(skill, manifest) {
|
|
268
|
+
let resolvedPath;
|
|
269
|
+
try {
|
|
270
|
+
resolvedPath = resolveSkillPath(skill.path);
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
return {
|
|
274
|
+
findings: [{
|
|
275
|
+
id: "SCAN-ERR-01",
|
|
276
|
+
category: "SC",
|
|
277
|
+
asixx: "ASI04",
|
|
278
|
+
severity: "medium",
|
|
279
|
+
file: skill.path,
|
|
280
|
+
message: "Could not resolve skill path",
|
|
281
|
+
evidence: String(e)
|
|
282
|
+
}],
|
|
283
|
+
unreadableFiles: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const files = getSkillFiles(resolvedPath);
|
|
287
|
+
const findings = [];
|
|
288
|
+
const unreadableFiles = [];
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
const filename = basename(file);
|
|
291
|
+
try {
|
|
292
|
+
const content = readFileSync(file, "utf-8");
|
|
293
|
+
if (filename === "SKILL.md" || filename === "AGENTS.md") {
|
|
294
|
+
findings.push(...scanContent(content, file, PROMPT_INJECTION_PATTERNS));
|
|
295
|
+
findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_MD));
|
|
296
|
+
findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
|
|
297
|
+
findings.push(...scanContent(content, file, BEHAVIORAL_PATTERNS));
|
|
298
|
+
findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
|
|
299
|
+
findings.push(...scanCodeBlocksInMarkdown(content, file));
|
|
300
|
+
}
|
|
301
|
+
else if (isCodeFile(file)) {
|
|
302
|
+
findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_CODE));
|
|
303
|
+
findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
|
|
304
|
+
findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
|
|
305
|
+
findings.push(...scanContent(content, file, SECRET_PATTERNS));
|
|
306
|
+
findings.push(...scanContent(content, file, TOOL_MISUSE_PATTERNS));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
unreadableFiles.push(file);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Add findings for unreadable files
|
|
314
|
+
if (unreadableFiles.length > 0) {
|
|
315
|
+
findings.push({
|
|
316
|
+
id: "SCAN-ERR-02",
|
|
317
|
+
category: "SC",
|
|
318
|
+
asixx: "ASI04",
|
|
319
|
+
severity: "medium",
|
|
320
|
+
file: resolvedPath,
|
|
321
|
+
message: `Could not read ${unreadableFiles.length} file(s) - security scan incomplete`,
|
|
322
|
+
evidence: unreadableFiles.join(", ")
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (files.length === 0) {
|
|
326
|
+
findings.push({
|
|
327
|
+
id: "SCAN-ERR-03",
|
|
328
|
+
category: "SC",
|
|
329
|
+
asixx: "ASI04",
|
|
330
|
+
severity: "medium",
|
|
331
|
+
file: resolvedPath,
|
|
332
|
+
message: "No files found in skill directory",
|
|
333
|
+
evidence: resolvedPath
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Provenance checks (origin is optional metadata, not spec-required)
|
|
337
|
+
if (manifest?.origin) {
|
|
338
|
+
findings.push(...checkProvenance(manifest.origin, resolvedPath));
|
|
339
|
+
}
|
|
340
|
+
return { findings, unreadableFiles };
|
|
341
|
+
}
|