agent-security-lens 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/.env.example +10 -0
- package/.mcp/server.json +42 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +17 -0
- package/PRIVACY.md +37 -0
- package/README.md +150 -0
- package/RELEASE-MANIFEST.json +449 -0
- package/SECURITY.md +24 -0
- package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
- package/bin/agent-security-lens.mjs +117 -0
- package/data/ecosystems/agent-candidates.json +230 -0
- package/data/intelligence/components.json +22989 -0
- package/data/intelligence/security-evaluation-standard.json +221 -0
- package/data/recommendations/core/recommendations.json +256 -0
- package/data/trust/signal-taxonomy.json +107 -0
- package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
- package/examples/dot-hermes/.hermes/config.json +17 -0
- package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
- package/examples/hermes-like/.env.example +2 -0
- package/examples/hermes-like/config.json +37 -0
- package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
- package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
- package/examples/openclaw-like/.env.example +2 -0
- package/examples/openclaw-like/AGENTS.md +7 -0
- package/examples/openclaw-like/openclaw.json +28 -0
- package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
- package/llms.txt +25 -0
- package/package.json +50 -0
- package/profiles/generic-agent/profile.json +19 -0
- package/profiles/hermes-like/profile.json +23 -0
- package/profiles/mcp-server/profile.json +18 -0
- package/profiles/openclaw-like/profile.json +22 -0
- package/profiles/skill-runtime/profile.json +19 -0
- package/rule-packs/core/rules.json +82 -0
- package/rule-packs/hermes/rules.json +44 -0
- package/rule-packs/mcp/rules.json +65 -0
- package/rule-packs/openclaw/rules.json +46 -0
- package/rule-packs/skills/rules.json +45 -0
- package/schemas/agent-install-decision.schema.json +432 -0
- package/schemas/agent-usage-event.schema.json +45 -0
- package/schemas/assessment-result.schema.json +361 -0
- package/schemas/comparison-result.schema.json +113 -0
- package/schemas/component-alternative-graph.schema.json +187 -0
- package/schemas/component-intelligence.schema.json +93 -0
- package/schemas/decision-feedback.schema.json +49 -0
- package/schemas/ecosystem-candidate-registry.schema.json +98 -0
- package/schemas/profile.schema.json +65 -0
- package/schemas/recommendation-pack.schema.json +114 -0
- package/schemas/rule-pack.schema.json +113 -0
- package/schemas/trust-signal-taxonomy.schema.json +68 -0
- package/scripts/verify-examples.mjs +121 -0
- package/scripts/verify-mcp-server.mjs +278 -0
- package/scripts/verify-registry.mjs +264 -0
- package/server.json +42 -0
- package/src/assessment/assess.mjs +108 -0
- package/src/assessment/discover-targets.mjs +127 -0
- package/src/assessment/risk-domains.mjs +83 -0
- package/src/assessment/summarize.mjs +57 -0
- package/src/core/files.mjs +74 -0
- package/src/intelligence/cloud-client.mjs +260 -0
- package/src/intelligence/component-intelligence.mjs +358 -0
- package/src/intelligence/decision-engine.mjs +772 -0
- package/src/intelligence/finding-context.mjs +180 -0
- package/src/intelligence/safety-score-v0.2.mjs +294 -0
- package/src/observations/json-observations.mjs +211 -0
- package/src/observations/observation-rules.mjs +157 -0
- package/src/profiles/load-profiles.mjs +130 -0
- package/src/recommendations/component-alternative-graph.mjs +94 -0
- package/src/recommendations/load-recommendations.mjs +17 -0
- package/src/recommendations/match-recommendations.mjs +79 -0
- package/src/report/comparison-console.mjs +71 -0
- package/src/report/console.mjs +103 -0
- package/src/report/markdown.mjs +145 -0
- package/src/results/compare-results.mjs +106 -0
- package/src/results/save-result.mjs +29 -0
- package/src/rules/load-rules.mjs +22 -0
- package/src/rules/match-rules.mjs +99 -0
- package/src/rules/supersedes.mjs +39 -0
- package/src/store/assessment-store.mjs +78 -0
- package/src/trust/derive-trust-signals.mjs +73 -0
- package/src/trust/load-trust-signals.mjs +17 -0
package/server.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.professor2k8/agent-security-lens",
|
|
4
|
+
"title": "AgentSecurityLens",
|
|
5
|
+
"description": "Security intelligence MCP for agents to review MCPs, Skills and tools before installation.",
|
|
6
|
+
"status": "active",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/professor2k8/agent-security-lens",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"identifier": "agent-security-lens",
|
|
16
|
+
"version": "0.1.0",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
},
|
|
20
|
+
"environmentVariables": [
|
|
21
|
+
{
|
|
22
|
+
"name": "ASL_API_URL",
|
|
23
|
+
"description": "AgentSecurityLens Cloud Intelligence API URL.",
|
|
24
|
+
"isRequired": false,
|
|
25
|
+
"default": "https://api.agentsecuritylens.com"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "ASL_API_KEY",
|
|
29
|
+
"description": "Optional API key for Team, Pro or Enterprise use.",
|
|
30
|
+
"isRequired": false,
|
|
31
|
+
"isSecret": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "ASL_MODE",
|
|
35
|
+
"description": "Set to local for offline fallback mode.",
|
|
36
|
+
"isRequired": false,
|
|
37
|
+
"default": "online"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { discoverFiles } from "../core/files.mjs";
|
|
5
|
+
import { extractJsonObservations } from "../observations/json-observations.mjs";
|
|
6
|
+
import { findingsFromObservations } from "../observations/observation-rules.mjs";
|
|
7
|
+
import { loadProfiles, resolveProfileSelection } from "../profiles/load-profiles.mjs";
|
|
8
|
+
import { loadRecommendationPacks } from "../recommendations/load-recommendations.mjs";
|
|
9
|
+
import { applyRecommendations } from "../recommendations/match-recommendations.mjs";
|
|
10
|
+
import { loadRulePacks } from "../rules/load-rules.mjs";
|
|
11
|
+
import { matchRules } from "../rules/match-rules.mjs";
|
|
12
|
+
import { applySupersedes } from "../rules/supersedes.mjs";
|
|
13
|
+
import { deriveTrustSignals, summarizeTrustSignals } from "../trust/derive-trust-signals.mjs";
|
|
14
|
+
import { loadTrustSignalTaxonomies } from "../trust/load-trust-signals.mjs";
|
|
15
|
+
import { groupByRiskDomain } from "./risk-domains.mjs";
|
|
16
|
+
import { summarize } from "./summarize.mjs";
|
|
17
|
+
|
|
18
|
+
export async function assess({ targetPath, requestedProfile }) {
|
|
19
|
+
const startedAt = new Date().toISOString();
|
|
20
|
+
const root = resolve(targetPath);
|
|
21
|
+
const profiles = await loadProfiles();
|
|
22
|
+
const profileSelection = await resolveProfileSelection({ profiles, requestedProfile, root });
|
|
23
|
+
const matchedProfiles = profileSelection.profiles;
|
|
24
|
+
const rulePacks = await loadRulePacks(matchedProfiles);
|
|
25
|
+
const recommendationPacks = await loadRecommendationPacks();
|
|
26
|
+
const trustSignalTaxonomies = await loadTrustSignalTaxonomies();
|
|
27
|
+
const files = await discoverFiles(root);
|
|
28
|
+
const textFindings = await matchRules({ root, files, profiles: matchedProfiles, rulePacks });
|
|
29
|
+
const observations = await extractJsonObservations(files);
|
|
30
|
+
const observationFindings = findingsFromObservations({
|
|
31
|
+
observations,
|
|
32
|
+
profileIds: matchedProfiles.map((profile) => profile.id)
|
|
33
|
+
});
|
|
34
|
+
const findings = applyRecommendations(
|
|
35
|
+
applySupersedes([...observationFindings, ...textFindings]),
|
|
36
|
+
recommendationPacks
|
|
37
|
+
);
|
|
38
|
+
const trustSignals = deriveTrustSignals({ findings, taxonomies: trustSignalTaxonomies });
|
|
39
|
+
const summary = summarize(findings, matchedProfiles);
|
|
40
|
+
summary.trust_signal_summary = summarizeTrustSignals(trustSignals);
|
|
41
|
+
const riskDomains = groupByRiskDomain(findings);
|
|
42
|
+
const completedAt = new Date().toISOString();
|
|
43
|
+
const assessmentId = createHash("sha256")
|
|
44
|
+
.update([root, startedAt, completedAt, profileSelection.selected_profile].join("|"))
|
|
45
|
+
.digest("hex")
|
|
46
|
+
.slice(0, 16);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
schema_version: "0.1.0",
|
|
50
|
+
tool: {
|
|
51
|
+
name: "AgentSecurityLens",
|
|
52
|
+
version: "0.1.0-alpha.0"
|
|
53
|
+
},
|
|
54
|
+
assessment: {
|
|
55
|
+
id: assessmentId,
|
|
56
|
+
target_path: root,
|
|
57
|
+
target_url: pathToFileURL(root).href,
|
|
58
|
+
started_at: startedAt,
|
|
59
|
+
completed_at: completedAt
|
|
60
|
+
},
|
|
61
|
+
lineage: {
|
|
62
|
+
profile_selection: {
|
|
63
|
+
mode: profileSelection.mode,
|
|
64
|
+
requested_profile: profileSelection.requested_profile,
|
|
65
|
+
selected_profile: profileSelection.selected_profile,
|
|
66
|
+
detection_signals: profileSelection.detection_signals
|
|
67
|
+
},
|
|
68
|
+
algorithms: [
|
|
69
|
+
{ id: "json-observation-extractor", version: "0.1.0" },
|
|
70
|
+
{ id: "observation-to-finding-rules", version: "0.1.0" },
|
|
71
|
+
{ id: "finding-supersedes", version: "0.1.0" },
|
|
72
|
+
{ id: "recommendation-matcher", version: "0.1.0" },
|
|
73
|
+
{ id: "trust-signal-deriver", version: "0.1.0" },
|
|
74
|
+
{ id: summary.scoring_model, version: "0.1.0" }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
profiles: matchedProfiles.map((profile) => ({
|
|
78
|
+
id: profile.id,
|
|
79
|
+
version: profile.version,
|
|
80
|
+
status: profile.status,
|
|
81
|
+
confidence: profile.confidence,
|
|
82
|
+
coverage: profile.coverage,
|
|
83
|
+
known_limitations: profile.known_limitations
|
|
84
|
+
})),
|
|
85
|
+
rule_packs: rulePacks.map((pack) => ({
|
|
86
|
+
id: pack.id,
|
|
87
|
+
version: pack.version,
|
|
88
|
+
rule_count: pack.rules.length
|
|
89
|
+
})),
|
|
90
|
+
recommendation_packs: recommendationPacks.map((pack) => ({
|
|
91
|
+
id: pack.id,
|
|
92
|
+
version: pack.version,
|
|
93
|
+
status: pack.status,
|
|
94
|
+
recommendation_count: pack.recommendations.length
|
|
95
|
+
})),
|
|
96
|
+
trust_signal_taxonomies: trustSignalTaxonomies.map((taxonomy) => ({
|
|
97
|
+
id: taxonomy.id,
|
|
98
|
+
version: taxonomy.version,
|
|
99
|
+
status: taxonomy.status,
|
|
100
|
+
signal_count: taxonomy.signals.length
|
|
101
|
+
})),
|
|
102
|
+
trust_signals: trustSignals,
|
|
103
|
+
observations,
|
|
104
|
+
summary,
|
|
105
|
+
risk_domains: riskDomains,
|
|
106
|
+
findings
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".agentsecuritylens",
|
|
7
|
+
".agentsecuritylens-test",
|
|
8
|
+
"node_modules",
|
|
9
|
+
"dist",
|
|
10
|
+
"build",
|
|
11
|
+
".next",
|
|
12
|
+
".cache",
|
|
13
|
+
"__pycache__"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const MARKERS = [
|
|
17
|
+
{ profile: "openclaw-like", file: "openclaw.json", reason: "OpenClaw config" },
|
|
18
|
+
{ profile: "openclaw-like", file: ".openclaw/openclaw.json", reason: "OpenClaw local config" },
|
|
19
|
+
{ profile: "openclaw-like", file: "SOUL.md", reason: "OpenClaw instruction file" },
|
|
20
|
+
{ profile: "openclaw-like", file: "TOOLS.md", reason: "OpenClaw tool manifest" },
|
|
21
|
+
{ profile: "openclaw-like", file: "workspace/skills", reason: "OpenClaw skills workspace" },
|
|
22
|
+
{ profile: "hermes-like", file: ".hermes", reason: "Hermes config directory" },
|
|
23
|
+
{ profile: "hermes-like", file: ".hermes/config.json", reason: "Hermes JSON config" },
|
|
24
|
+
{ profile: "hermes-like", file: ".hermes/config.yaml", reason: "Hermes YAML config" },
|
|
25
|
+
{ profile: "hermes-like", file: "optional-mcps", reason: "Hermes optional MCPs" },
|
|
26
|
+
{ profile: "hermes-like", file: "optional-skills", reason: "Hermes optional skills" },
|
|
27
|
+
{ profile: "hermes-like", file: "gateway", reason: "Hermes gateway" },
|
|
28
|
+
{ profile: "hermes-like", file: "cron", reason: "Hermes scheduled runs" },
|
|
29
|
+
{ profile: "skill-runtime", file: "SKILL.md", reason: "Agent skill definition" },
|
|
30
|
+
{ profile: "mcp-server", file: "mcp.json", reason: "MCP manifest" }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
async function listEntries(dir) {
|
|
34
|
+
try {
|
|
35
|
+
return await readdir(dir, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function hasPath(root, path) {
|
|
42
|
+
const parts = path.split("/");
|
|
43
|
+
let entries = await listEntries(root);
|
|
44
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
45
|
+
const part = parts[index];
|
|
46
|
+
const entry = entries.find((item) => item.name.toLowerCase() === part.toLowerCase());
|
|
47
|
+
if (!entry) return false;
|
|
48
|
+
if (index === parts.length - 1) return true;
|
|
49
|
+
if (!entry.isDirectory()) return false;
|
|
50
|
+
entries = await listEntries(join(root, entry.name));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parentTargetForMarker(dir, markerFile) {
|
|
56
|
+
if (markerFile === "SKILL.md") return dir;
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function confidenceFor(signals) {
|
|
61
|
+
if (signals.length >= 3) return 0.9;
|
|
62
|
+
if (signals.length === 2) return 0.78;
|
|
63
|
+
return 0.62;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function labelFor(path, workspaceRoot) {
|
|
67
|
+
const rel = relative(workspaceRoot, path).replaceAll("\\", "/");
|
|
68
|
+
return rel || ".";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function discoverTargets({ workspacePath, maxDepth = 4 }) {
|
|
72
|
+
const workspaceRoot = resolve(workspacePath);
|
|
73
|
+
const candidates = new Map();
|
|
74
|
+
|
|
75
|
+
async function inspectDir(dir, depth) {
|
|
76
|
+
const matched = [];
|
|
77
|
+
for (const marker of MARKERS) {
|
|
78
|
+
if (await hasPath(dir, marker.file)) {
|
|
79
|
+
matched.push(marker);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (matched.length) {
|
|
84
|
+
const targetPath = parentTargetForMarker(dir, matched[0].file);
|
|
85
|
+
const existing = candidates.get(targetPath) || {
|
|
86
|
+
path: targetPath,
|
|
87
|
+
label: labelFor(targetPath, workspaceRoot),
|
|
88
|
+
profile: matched[0].profile,
|
|
89
|
+
signals: []
|
|
90
|
+
};
|
|
91
|
+
for (const marker of matched) {
|
|
92
|
+
existing.signals.push({
|
|
93
|
+
profile: marker.profile,
|
|
94
|
+
marker: marker.file,
|
|
95
|
+
reason: marker.reason
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const byProfile = new Map();
|
|
99
|
+
for (const signal of existing.signals) {
|
|
100
|
+
byProfile.set(signal.profile, (byProfile.get(signal.profile) || 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
existing.profile = [...byProfile.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
103
|
+
existing.confidence = confidenceFor(existing.signals);
|
|
104
|
+
candidates.set(targetPath, existing);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (depth >= maxDepth) return;
|
|
108
|
+
for (const entry of await listEntries(dir)) {
|
|
109
|
+
if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name)) continue;
|
|
110
|
+
await inspectDir(join(dir, entry.name), depth + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await inspectDir(workspaceRoot, 0);
|
|
115
|
+
const targets = [...candidates.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
116
|
+
const deduped = targets.filter((target) => {
|
|
117
|
+
return !targets.some((other) => {
|
|
118
|
+
if (other === target || other.profile !== target.profile) return false;
|
|
119
|
+
const rel = relative(other.path, target.path);
|
|
120
|
+
return rel && !rel.startsWith("..") && rel !== "." && !rel.includes(":");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
workspace_path: workspaceRoot,
|
|
125
|
+
targets: deduped
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const DOMAIN_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
id: "mcp",
|
|
4
|
+
title: "MCP",
|
|
5
|
+
match: (finding) => finding.permissions.includes("mcp-tool-access") || finding.rule_id.includes("mcp")
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
id: "skills",
|
|
9
|
+
title: "Skills",
|
|
10
|
+
match: (finding) => finding.permissions.includes("skill-installation") || finding.rule_id.includes("skill")
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "remote-triggers",
|
|
14
|
+
title: "Remote Triggers",
|
|
15
|
+
match: (finding) => finding.permissions.includes("remote-trigger")
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "scheduler",
|
|
19
|
+
title: "Scheduler",
|
|
20
|
+
match: (finding) => finding.permissions.includes("scheduled-execution")
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "credentials",
|
|
24
|
+
title: "Credentials",
|
|
25
|
+
match: (finding) => finding.permissions.includes("credential-access") || finding.permissions.includes("env-read")
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "execution",
|
|
29
|
+
title: "Execution",
|
|
30
|
+
match: (finding) => finding.category === "execution-risk"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "supply-chain",
|
|
34
|
+
title: "Supply Chain",
|
|
35
|
+
match: (finding) => finding.category === "supply-chain-risk"
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const severityRank = {
|
|
40
|
+
critical: 5,
|
|
41
|
+
high: 4,
|
|
42
|
+
medium: 3,
|
|
43
|
+
low: 2,
|
|
44
|
+
info: 1
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function highestSeverity(findings) {
|
|
48
|
+
return findings.reduce((highest, finding) => {
|
|
49
|
+
if (!highest) return finding.severity;
|
|
50
|
+
return severityRank[finding.severity] > severityRank[highest] ? finding.severity : highest;
|
|
51
|
+
}, null);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function groupByRiskDomain(findings) {
|
|
55
|
+
const domains = [];
|
|
56
|
+
const assigned = new Set();
|
|
57
|
+
|
|
58
|
+
for (const definition of DOMAIN_DEFINITIONS) {
|
|
59
|
+
const domainFindings = findings.filter((finding) => definition.match(finding));
|
|
60
|
+
if (!domainFindings.length) continue;
|
|
61
|
+
for (const finding of domainFindings) assigned.add(finding.id);
|
|
62
|
+
domains.push({
|
|
63
|
+
id: definition.id,
|
|
64
|
+
title: definition.title,
|
|
65
|
+
count: domainFindings.length,
|
|
66
|
+
highest_severity: highestSeverity(domainFindings),
|
|
67
|
+
findings: domainFindings.map((finding) => finding.id)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const otherFindings = findings.filter((finding) => !assigned.has(finding.id));
|
|
72
|
+
if (otherFindings.length) {
|
|
73
|
+
domains.push({
|
|
74
|
+
id: "other",
|
|
75
|
+
title: "Other",
|
|
76
|
+
count: otherFindings.length,
|
|
77
|
+
highest_severity: highestSeverity(otherFindings),
|
|
78
|
+
findings: otherFindings.map((finding) => finding.id)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return domains;
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const severityWeights = {
|
|
2
|
+
critical: 22,
|
|
3
|
+
high: 12,
|
|
4
|
+
medium: 5,
|
|
5
|
+
low: 1,
|
|
6
|
+
info: 0
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function categoryMultiplier(category) {
|
|
10
|
+
if (category === "execution-risk") return 1.15;
|
|
11
|
+
if (category === "data-exposure-risk") return 1.1;
|
|
12
|
+
if (category === "supply-chain-risk") return 1.05;
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function summarize(findings, profiles = []) {
|
|
17
|
+
const bySeverity = {};
|
|
18
|
+
const byCategory = {};
|
|
19
|
+
|
|
20
|
+
for (const finding of findings) {
|
|
21
|
+
bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
|
|
22
|
+
byCategory[finding.category] = (byCategory[finding.category] || 0) + 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rawPenalty = findings.reduce((total, finding) => {
|
|
26
|
+
const base = severityWeights[finding.severity] || 0;
|
|
27
|
+
const confidence = typeof finding.confidence === "number" ? finding.confidence : 0.7;
|
|
28
|
+
return total + base * confidence * categoryMultiplier(finding.category);
|
|
29
|
+
}, 0);
|
|
30
|
+
|
|
31
|
+
const profileCoverageAverage = profiles.length
|
|
32
|
+
? profiles.reduce((total, profile) => total + (profile.coverage || 0), 0) / profiles.length
|
|
33
|
+
: 0.5;
|
|
34
|
+
const lowCoveragePenalty = profileCoverageAverage < 0.5 ? 6 : 0;
|
|
35
|
+
const penalty = Math.round(rawPenalty + lowCoveragePenalty);
|
|
36
|
+
|
|
37
|
+
const topFindings = findings
|
|
38
|
+
.slice()
|
|
39
|
+
.sort((a, b) => (severityWeights[b.severity] || 0) - (severityWeights[a.severity] || 0))
|
|
40
|
+
.slice(0, 5)
|
|
41
|
+
.map((finding) => ({
|
|
42
|
+
title: finding.title,
|
|
43
|
+
severity: finding.severity,
|
|
44
|
+
category: finding.category,
|
|
45
|
+
evidence: finding.evidence
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
total_findings: findings.length,
|
|
50
|
+
by_severity: bySeverity,
|
|
51
|
+
by_category: byCategory,
|
|
52
|
+
trust_score: Math.max(0, 100 - penalty),
|
|
53
|
+
scoring_model: "simple-penalty@0.1.0",
|
|
54
|
+
profile_coverage_average: Number(profileCoverageAverage.toFixed(2)),
|
|
55
|
+
top_findings: topFindings
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_IGNORES = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
"node_modules",
|
|
7
|
+
"dist",
|
|
8
|
+
"build",
|
|
9
|
+
".next",
|
|
10
|
+
".cache",
|
|
11
|
+
"__pycache__"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
15
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
16
|
+
"",
|
|
17
|
+
".md",
|
|
18
|
+
".txt",
|
|
19
|
+
".json",
|
|
20
|
+
".jsonc",
|
|
21
|
+
".yaml",
|
|
22
|
+
".yml",
|
|
23
|
+
".toml",
|
|
24
|
+
".js",
|
|
25
|
+
".mjs",
|
|
26
|
+
".cjs",
|
|
27
|
+
".ts",
|
|
28
|
+
".tsx",
|
|
29
|
+
".py",
|
|
30
|
+
".sh",
|
|
31
|
+
".bash",
|
|
32
|
+
".zsh",
|
|
33
|
+
".ps1",
|
|
34
|
+
".env",
|
|
35
|
+
".example"
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function extnameLoose(path) {
|
|
39
|
+
const name = path.toLowerCase();
|
|
40
|
+
if (name.endsWith(".env") || name.includes(".env.")) return ".env";
|
|
41
|
+
const index = name.lastIndexOf(".");
|
|
42
|
+
return index === -1 ? "" : name.slice(index);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function discoverFiles(root) {
|
|
46
|
+
const files = [];
|
|
47
|
+
|
|
48
|
+
async function walk(dir) {
|
|
49
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (DEFAULT_IGNORES.has(entry.name)) continue;
|
|
52
|
+
const fullPath = join(dir, entry.name);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await walk(fullPath);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!entry.isFile()) continue;
|
|
58
|
+
|
|
59
|
+
const info = await stat(fullPath);
|
|
60
|
+
if (info.size > MAX_FILE_SIZE_BYTES) continue;
|
|
61
|
+
const ext = extnameLoose(fullPath);
|
|
62
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
|
|
63
|
+
|
|
64
|
+
files.push({
|
|
65
|
+
path: fullPath,
|
|
66
|
+
relative_path: relative(root, fullPath).replaceAll("\\", "/"),
|
|
67
|
+
size: info.size
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await walk(root);
|
|
73
|
+
return files;
|
|
74
|
+
}
|