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.
Files changed (81) hide show
  1. package/.env.example +10 -0
  2. package/.mcp/server.json +42 -0
  3. package/CHANGELOG.md +17 -0
  4. package/LICENSE +17 -0
  5. package/PRIVACY.md +37 -0
  6. package/README.md +150 -0
  7. package/RELEASE-MANIFEST.json +449 -0
  8. package/SECURITY.md +24 -0
  9. package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
  10. package/bin/agent-security-lens.mjs +117 -0
  11. package/data/ecosystems/agent-candidates.json +230 -0
  12. package/data/intelligence/components.json +22989 -0
  13. package/data/intelligence/security-evaluation-standard.json +221 -0
  14. package/data/recommendations/core/recommendations.json +256 -0
  15. package/data/trust/signal-taxonomy.json +107 -0
  16. package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
  17. package/examples/dot-hermes/.hermes/config.json +17 -0
  18. package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
  19. package/examples/hermes-like/.env.example +2 -0
  20. package/examples/hermes-like/config.json +37 -0
  21. package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
  22. package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
  23. package/examples/openclaw-like/.env.example +2 -0
  24. package/examples/openclaw-like/AGENTS.md +7 -0
  25. package/examples/openclaw-like/openclaw.json +28 -0
  26. package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
  27. package/llms.txt +25 -0
  28. package/package.json +50 -0
  29. package/profiles/generic-agent/profile.json +19 -0
  30. package/profiles/hermes-like/profile.json +23 -0
  31. package/profiles/mcp-server/profile.json +18 -0
  32. package/profiles/openclaw-like/profile.json +22 -0
  33. package/profiles/skill-runtime/profile.json +19 -0
  34. package/rule-packs/core/rules.json +82 -0
  35. package/rule-packs/hermes/rules.json +44 -0
  36. package/rule-packs/mcp/rules.json +65 -0
  37. package/rule-packs/openclaw/rules.json +46 -0
  38. package/rule-packs/skills/rules.json +45 -0
  39. package/schemas/agent-install-decision.schema.json +432 -0
  40. package/schemas/agent-usage-event.schema.json +45 -0
  41. package/schemas/assessment-result.schema.json +361 -0
  42. package/schemas/comparison-result.schema.json +113 -0
  43. package/schemas/component-alternative-graph.schema.json +187 -0
  44. package/schemas/component-intelligence.schema.json +93 -0
  45. package/schemas/decision-feedback.schema.json +49 -0
  46. package/schemas/ecosystem-candidate-registry.schema.json +98 -0
  47. package/schemas/profile.schema.json +65 -0
  48. package/schemas/recommendation-pack.schema.json +114 -0
  49. package/schemas/rule-pack.schema.json +113 -0
  50. package/schemas/trust-signal-taxonomy.schema.json +68 -0
  51. package/scripts/verify-examples.mjs +121 -0
  52. package/scripts/verify-mcp-server.mjs +278 -0
  53. package/scripts/verify-registry.mjs +264 -0
  54. package/server.json +42 -0
  55. package/src/assessment/assess.mjs +108 -0
  56. package/src/assessment/discover-targets.mjs +127 -0
  57. package/src/assessment/risk-domains.mjs +83 -0
  58. package/src/assessment/summarize.mjs +57 -0
  59. package/src/core/files.mjs +74 -0
  60. package/src/intelligence/cloud-client.mjs +260 -0
  61. package/src/intelligence/component-intelligence.mjs +358 -0
  62. package/src/intelligence/decision-engine.mjs +772 -0
  63. package/src/intelligence/finding-context.mjs +180 -0
  64. package/src/intelligence/safety-score-v0.2.mjs +294 -0
  65. package/src/observations/json-observations.mjs +211 -0
  66. package/src/observations/observation-rules.mjs +157 -0
  67. package/src/profiles/load-profiles.mjs +130 -0
  68. package/src/recommendations/component-alternative-graph.mjs +94 -0
  69. package/src/recommendations/load-recommendations.mjs +17 -0
  70. package/src/recommendations/match-recommendations.mjs +79 -0
  71. package/src/report/comparison-console.mjs +71 -0
  72. package/src/report/console.mjs +103 -0
  73. package/src/report/markdown.mjs +145 -0
  74. package/src/results/compare-results.mjs +106 -0
  75. package/src/results/save-result.mjs +29 -0
  76. package/src/rules/load-rules.mjs +22 -0
  77. package/src/rules/match-rules.mjs +99 -0
  78. package/src/rules/supersedes.mjs +39 -0
  79. package/src/store/assessment-store.mjs +78 -0
  80. package/src/trust/derive-trust-signals.mjs +73 -0
  81. package/src/trust/load-trust-signals.mjs +17 -0
@@ -0,0 +1,145 @@
1
+ export function renderMarkdown(result) {
2
+ const lines = [];
3
+ lines.push("# AgentSecurityLens Security Assessment");
4
+ lines.push("");
5
+ lines.push(`- Assessment ID: \`${result.assessment.id}\``);
6
+ lines.push(`- Target: \`${result.assessment.target_path}\``);
7
+ lines.push(`- Trust Score: **${result.summary.trust_score}/100**`);
8
+ lines.push(`- Findings: **${result.summary.total_findings}**`);
9
+ lines.push(`- Categories: ${Object.entries(result.summary.by_category).map(([key, value]) => `\`${key}\`: ${value}`).join(", ") || "none"}`);
10
+ lines.push(`- Profile Coverage Avg: **${result.summary.profile_coverage_average}**`);
11
+ lines.push("");
12
+ lines.push("## Profiles");
13
+ lines.push("");
14
+ for (const profile of result.profiles) {
15
+ lines.push(`- \`${profile.id}@${profile.version}\` (${profile.status}, confidence ${profile.confidence})`);
16
+ }
17
+ lines.push("");
18
+ lines.push("## Rule Packs");
19
+ lines.push("");
20
+ for (const pack of result.rule_packs) {
21
+ lines.push(`- \`${pack.id}@${pack.version}\` (${pack.rule_count} rules)`);
22
+ }
23
+ lines.push("");
24
+ if (result.recommendation_packs?.length) {
25
+ lines.push("## Recommendation Packs");
26
+ lines.push("");
27
+ for (const pack of result.recommendation_packs) {
28
+ lines.push(`- \`${pack.id}@${pack.version}\` (${pack.status}, ${pack.recommendation_count} recommendations)`);
29
+ }
30
+ lines.push("");
31
+ }
32
+ if (result.trust_signal_taxonomies?.length) {
33
+ lines.push("## Trust Signal Taxonomies");
34
+ lines.push("");
35
+ for (const taxonomy of result.trust_signal_taxonomies) {
36
+ lines.push(`- \`${taxonomy.id}@${taxonomy.version}\` (${taxonomy.status}, ${taxonomy.signal_count} signals)`);
37
+ }
38
+ lines.push("");
39
+ }
40
+ lines.push("## Profile Selection");
41
+ lines.push("");
42
+ lines.push(`- Mode: \`${result.lineage.profile_selection.mode}\``);
43
+ lines.push(`- Selected: \`${result.lineage.profile_selection.selected_profile}\``);
44
+ if (result.lineage.profile_selection.detection_signals.length) {
45
+ const signals = result.lineage.profile_selection.detection_signals
46
+ .map((signal) => `\`${signal.type}:${signal.value}\``)
47
+ .join(", ");
48
+ lines.push(`- Signals: ${signals}`);
49
+ }
50
+ lines.push("");
51
+
52
+ if (result.risk_domains?.length) {
53
+ lines.push("## Risk Domains");
54
+ lines.push("");
55
+ for (const domain of result.risk_domains) {
56
+ lines.push(`- **${domain.title}**: ${domain.count} finding(s), highest \`${domain.highest_severity}\``);
57
+ }
58
+ lines.push("");
59
+ }
60
+
61
+ if (result.trust_signals?.length) {
62
+ const summary = result.summary.trust_signal_summary;
63
+ lines.push("## Trust Signals");
64
+ lines.push("");
65
+ lines.push(`- Total: **${summary.total}**`);
66
+ lines.push(`- Net weight: **${summary.net_weight}**`);
67
+ lines.push(
68
+ `- Direction: ${Object.entries(summary.by_direction).map(([key, value]) => `\`${key}\`: ${value}`).join(", ")}`
69
+ );
70
+ lines.push("");
71
+ for (const signal of result.trust_signals.slice(0, 5)) {
72
+ lines.push(`- \`${signal.signal_id}\` ${signal.title} (${signal.weight}) at \`${signal.evidence.path}:${signal.evidence.line}\``);
73
+ }
74
+ lines.push("");
75
+ }
76
+
77
+ lines.push("## Detailed Findings");
78
+ lines.push("");
79
+
80
+ if (result.findings.length === 0) {
81
+ lines.push("No risk findings detected by the current rule packs.");
82
+ lines.push("");
83
+ lines.push("> Known limitation: this does not prove the agent environment is safe.");
84
+ return lines.join("\n");
85
+ }
86
+
87
+ for (const finding of result.findings) {
88
+ lines.push(`### ${finding.title}`);
89
+ lines.push("");
90
+ lines.push(`- Severity: \`${finding.severity}\``);
91
+ lines.push(`- Category: \`${finding.category}\``);
92
+ lines.push(`- Permissions: ${finding.permissions.map((item) => `\`${item}\``).join(", ") || "none"}`);
93
+ lines.push(`- Evidence: \`${finding.evidence.path}:${finding.evidence.line}\``);
94
+ if (finding.evidence_items.length > 1) {
95
+ lines.push(`- Additional evidence: ${finding.evidence_items.length - 1} more match(es) in this file`);
96
+ }
97
+ lines.push("");
98
+ lines.push("**Why it matters**");
99
+ lines.push("");
100
+ lines.push(finding.why_it_matters);
101
+ lines.push("");
102
+ if (finding.recommended_actions.length) {
103
+ lines.push("**Recommended actions**");
104
+ lines.push("");
105
+ for (const action of finding.recommended_actions) lines.push(`- ${action}`);
106
+ lines.push("");
107
+ }
108
+ if (finding.recommended_alternatives.length) {
109
+ lines.push("**Recommended alternatives**");
110
+ lines.push("");
111
+ for (const alternative of finding.recommended_alternatives) lines.push(`- ${alternative}`);
112
+ lines.push("");
113
+ }
114
+ if (finding.migration_instruction) {
115
+ lines.push("**Copyable migration instruction**");
116
+ lines.push("");
117
+ lines.push("```txt");
118
+ lines.push(finding.migration_instruction);
119
+ lines.push("```");
120
+ lines.push("");
121
+ }
122
+ if (finding.recommendations?.length) {
123
+ const top = finding.recommendations[0];
124
+ lines.push("**Top recommendation**");
125
+ lines.push("");
126
+ lines.push(`- \`${top.id}\`: ${top.title}`);
127
+ lines.push(`- Type: \`${top.type}\`, confidence: \`${top.confidence}\``);
128
+ if (top.one_step_commands?.length) {
129
+ lines.push("");
130
+ lines.push("**One-step instruction**");
131
+ lines.push("");
132
+ lines.push("```txt");
133
+ lines.push(top.one_step_commands[0].command);
134
+ lines.push("```");
135
+ }
136
+ if (top.rollback_note) {
137
+ lines.push("");
138
+ lines.push(`Rollback note: ${top.rollback_note}`);
139
+ }
140
+ lines.push("");
141
+ }
142
+ }
143
+
144
+ return lines.join("\n");
145
+ }
@@ -0,0 +1,106 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ function findingKey(finding) {
4
+ return [
5
+ finding.rule_id,
6
+ finding.evidence?.path || "unknown-path",
7
+ finding.evidence?.preview || finding.title
8
+ ].join("|");
9
+ }
10
+
11
+ function summarizeFinding(finding) {
12
+ return {
13
+ id: finding.id,
14
+ rule_id: finding.rule_id,
15
+ title: finding.title,
16
+ severity: finding.severity,
17
+ category: finding.category,
18
+ evidence: finding.evidence
19
+ };
20
+ }
21
+
22
+ function indexById(items) {
23
+ return new Map(items.map((item) => [item.id, item]));
24
+ }
25
+
26
+ function diffVersionedItems(previousItems, currentItems) {
27
+ const previous = indexById(previousItems);
28
+ const current = indexById(currentItems);
29
+ const added = [];
30
+ const removed = [];
31
+ const changed = [];
32
+
33
+ for (const [id, item] of current.entries()) {
34
+ const oldItem = previous.get(id);
35
+ if (!oldItem) {
36
+ added.push(item);
37
+ } else if (oldItem.version !== item.version) {
38
+ changed.push({ id, previous_version: oldItem.version, current_version: item.version });
39
+ }
40
+ }
41
+
42
+ for (const [id, item] of previous.entries()) {
43
+ if (!current.has(id)) removed.push(item);
44
+ }
45
+
46
+ return { added, removed, changed };
47
+ }
48
+
49
+ export function compareAssessmentResults(previous, current) {
50
+ const previousFindings = new Map(previous.findings.map((finding) => [findingKey(finding), finding]));
51
+ const currentFindings = new Map(current.findings.map((finding) => [findingKey(finding), finding]));
52
+ const added = [];
53
+ const resolved = [];
54
+ const persistent = [];
55
+
56
+ for (const [key, finding] of currentFindings.entries()) {
57
+ if (previousFindings.has(key)) {
58
+ persistent.push(summarizeFinding(finding));
59
+ } else {
60
+ added.push(summarizeFinding(finding));
61
+ }
62
+ }
63
+
64
+ for (const [key, finding] of previousFindings.entries()) {
65
+ if (!currentFindings.has(key)) {
66
+ resolved.push(summarizeFinding(finding));
67
+ }
68
+ }
69
+
70
+ return {
71
+ schema_version: "0.1.0",
72
+ comparison: {
73
+ previous_assessment_id: previous.assessment.id,
74
+ current_assessment_id: current.assessment.id,
75
+ previous_completed_at: previous.assessment.completed_at,
76
+ current_completed_at: current.assessment.completed_at,
77
+ compared_at: new Date().toISOString()
78
+ },
79
+ score: {
80
+ previous: previous.summary.trust_score,
81
+ current: current.summary.trust_score,
82
+ delta: current.summary.trust_score - previous.summary.trust_score
83
+ },
84
+ finding_counts: {
85
+ previous: previous.findings.length,
86
+ current: current.findings.length,
87
+ added: added.length,
88
+ resolved: resolved.length,
89
+ persistent: persistent.length
90
+ },
91
+ profiles: diffVersionedItems(previous.profiles, current.profiles),
92
+ rule_packs: diffVersionedItems(previous.rule_packs, current.rule_packs),
93
+ recommendation_packs: diffVersionedItems(previous.recommendation_packs || [], current.recommendation_packs || []),
94
+ findings: {
95
+ added,
96
+ resolved,
97
+ persistent
98
+ }
99
+ };
100
+ }
101
+
102
+ export async function compareAssessmentFiles(previousPath, currentPath) {
103
+ const previous = JSON.parse(await readFile(previousPath, "utf8"));
104
+ const current = JSON.parse(await readFile(currentPath, "utf8"));
105
+ return compareAssessmentResults(previous, current);
106
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+
4
+ function safeTimestamp(value) {
5
+ return value.replace(/[:.]/g, "-");
6
+ }
7
+
8
+ function targetSlug(targetPath) {
9
+ return basename(targetPath).replace(/[^a-zA-Z0-9._-]+/g, "-") || "target";
10
+ }
11
+
12
+ export async function saveAssessmentResult({ result, outDir }) {
13
+ const baseDir = resolve(outDir || join(result.assessment.target_path, ".agentsecuritylens", "runs"));
14
+ await mkdir(baseDir, { recursive: true });
15
+
16
+ const fileName = [
17
+ safeTimestamp(result.assessment.started_at),
18
+ targetSlug(result.assessment.target_path),
19
+ result.assessment.id
20
+ ].join("__");
21
+ const outputPath = join(baseDir, `${fileName}.json`);
22
+
23
+ await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
24
+
25
+ return {
26
+ path: outputPath,
27
+ archived_at: new Date().toISOString()
28
+ };
29
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const ROOT = join(__dirname, "..", "..");
7
+
8
+ export async function loadRulePacks(profiles) {
9
+ const packIds = new Set();
10
+ for (const profile of profiles) {
11
+ for (const packId of profile.rule_packs || []) {
12
+ packIds.add(packId);
13
+ }
14
+ }
15
+
16
+ const packs = [];
17
+ for (const packId of packIds) {
18
+ const content = await readFile(join(ROOT, "rule-packs", packId, "rules.json"), "utf8");
19
+ packs.push(JSON.parse(content));
20
+ }
21
+ return packs;
22
+ }
@@ -0,0 +1,99 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ function fileMatches(rule, relativePath) {
4
+ const targets = rule.target_paths || ["**/*"];
5
+ return targets.some((target) => {
6
+ if (target === "**/*") return true;
7
+ if (target.endsWith("/**")) return relativePath.startsWith(target.slice(0, -3));
8
+ if (target.startsWith("**/")) return relativePath.endsWith(target.slice(3));
9
+ if (target.includes("*")) {
10
+ const regex = new RegExp(`^${target.replaceAll(".", "\\.").replaceAll("**", ".*").replaceAll("*", "[^/]*")}$`);
11
+ return regex.test(relativePath);
12
+ }
13
+ return relativePath === target || relativePath.endsWith(`/${target}`);
14
+ });
15
+ }
16
+
17
+ function createFinding({ rule, file, line, lineNumber, profileIds }) {
18
+ return {
19
+ id: `${rule.id}:${file.relative_path}`,
20
+ rule_id: rule.id,
21
+ title: rule.title,
22
+ category: rule.category,
23
+ severity: rule.severity,
24
+ confidence: rule.confidence,
25
+ permissions: rule.permissions || [],
26
+ profile_ids: profileIds,
27
+ why_it_matters: rule.why_it_matters,
28
+ recommended_actions: rule.recommended_actions || [],
29
+ recommended_alternatives: rule.recommended_alternatives || [],
30
+ migration_instruction: rule.migration_instruction || "",
31
+ supersedes: rule.supersedes || [],
32
+ evidence: {
33
+ path: file.relative_path,
34
+ line: lineNumber,
35
+ preview: line.trim().slice(0, 240)
36
+ },
37
+ evidence_items: [
38
+ {
39
+ path: file.relative_path,
40
+ line: lineNumber,
41
+ preview: line.trim().slice(0, 240)
42
+ }
43
+ ]
44
+ };
45
+ }
46
+
47
+ function mergeFinding(existing, incoming) {
48
+ const seen = new Set(existing.evidence_items.map((item) => `${item.path}:${item.line}`));
49
+ for (const item of incoming.evidence_items) {
50
+ const key = `${item.path}:${item.line}`;
51
+ if (seen.has(key)) continue;
52
+ existing.evidence_items.push(item);
53
+ seen.add(key);
54
+ }
55
+ existing.evidence = existing.evidence_items[0];
56
+ return existing;
57
+ }
58
+
59
+ export async function matchRules({ files, profiles, rulePacks }) {
60
+ const findingsByKey = new Map();
61
+ const profileIds = profiles.map((profile) => profile.id);
62
+
63
+ for (const file of files) {
64
+ const content = await readFile(file.path, "utf8");
65
+ const lines = content.split(/\r?\n/);
66
+
67
+ for (const pack of rulePacks) {
68
+ for (const rule of pack.rules) {
69
+ if (!fileMatches(rule, file.relative_path)) continue;
70
+
71
+ const patterns = rule.patterns || [];
72
+ let matchedForFile = false;
73
+ for (let index = 0; index < lines.length; index += 1) {
74
+ const line = lines[index];
75
+ const matched = patterns.some((pattern) => new RegExp(pattern, "i").test(line));
76
+ if (!matched) continue;
77
+ const finding = createFinding({
78
+ rule,
79
+ file,
80
+ line,
81
+ lineNumber: index + 1,
82
+ profileIds
83
+ });
84
+ const key = `${finding.rule_id}:${finding.evidence.path}`;
85
+ if (findingsByKey.has(key)) {
86
+ mergeFinding(findingsByKey.get(key), finding);
87
+ } else {
88
+ findingsByKey.set(key, finding);
89
+ }
90
+ matchedForFile = true;
91
+ if (rule.match_scope === "file") break;
92
+ }
93
+ if (matchedForFile && rule.match_scope === "file") continue;
94
+ }
95
+ }
96
+ }
97
+
98
+ return Array.from(findingsByKey.values());
99
+ }
@@ -0,0 +1,39 @@
1
+ function evidenceOverlaps(a, b) {
2
+ const pathsA = new Set((a.evidence_items || [a.evidence]).map((item) => item.path));
3
+ const pathsB = new Set((b.evidence_items || [b.evidence]).map((item) => item.path));
4
+ for (const path of pathsA) {
5
+ if (pathsB.has(path)) return true;
6
+ }
7
+ return false;
8
+ }
9
+
10
+ function dedupeByRulePath(findings) {
11
+ const byKey = new Map();
12
+ for (const finding of findings) {
13
+ const key = `${finding.rule_id}:${finding.evidence.path}`;
14
+ if (!byKey.has(key)) {
15
+ byKey.set(key, finding);
16
+ continue;
17
+ }
18
+ const existing = byKey.get(key);
19
+ const seen = new Set((existing.evidence_items || [existing.evidence]).map((item) => `${item.path}:${item.preview}`));
20
+ for (const item of finding.evidence_items || [finding.evidence]) {
21
+ const itemKey = `${item.path}:${item.preview}`;
22
+ if (seen.has(itemKey)) continue;
23
+ existing.evidence_items.push(item);
24
+ seen.add(itemKey);
25
+ }
26
+ }
27
+ return Array.from(byKey.values());
28
+ }
29
+
30
+ export function applySupersedes(findings) {
31
+ const filtered = findings.filter((candidate) => {
32
+ return !findings.some((other) => {
33
+ if (other === candidate) return false;
34
+ if (!other.supersedes?.includes(candidate.rule_id)) return false;
35
+ return evidenceOverlaps(other, candidate);
36
+ });
37
+ });
38
+ return dedupeByRulePath(filtered);
39
+ }
@@ -0,0 +1,78 @@
1
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { compareAssessmentResults } from "../results/compare-results.mjs";
4
+
5
+ const DEFAULT_STORE_DIR = join(process.cwd(), ".agentsecuritylens", "store", "assessments");
6
+
7
+ function safeTimestamp(value) {
8
+ return value.replace(/[:.]/g, "-");
9
+ }
10
+
11
+ function targetSlug(targetPath) {
12
+ return basename(targetPath).replace(/[^a-zA-Z0-9._-]+/g, "-") || "target";
13
+ }
14
+
15
+ export function assessmentStoreDir() {
16
+ return resolve(process.env.ASL_STORE_DIR || DEFAULT_STORE_DIR);
17
+ }
18
+
19
+ function assessmentFileName(result) {
20
+ return [
21
+ safeTimestamp(result.assessment.started_at),
22
+ targetSlug(result.assessment.target_path),
23
+ result.assessment.id
24
+ ].join("__") + ".json";
25
+ }
26
+
27
+ function summaryFromResult(result, filePath) {
28
+ return {
29
+ id: result.assessment.id,
30
+ target_path: result.assessment.target_path,
31
+ completed_at: result.assessment.completed_at,
32
+ selected_profile: result.lineage.profile_selection.selected_profile,
33
+ trust_score: result.summary.trust_score,
34
+ total_findings: result.summary.total_findings,
35
+ trust_signals: result.trust_signals?.length || 0,
36
+ file_path: filePath
37
+ };
38
+ }
39
+
40
+ export async function saveToAssessmentStore(result) {
41
+ const dir = assessmentStoreDir();
42
+ await mkdir(dir, { recursive: true });
43
+ const filePath = join(dir, assessmentFileName(result));
44
+ await writeFile(filePath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
45
+ return summaryFromResult(result, filePath);
46
+ }
47
+
48
+ export async function listAssessmentStore() {
49
+ const dir = assessmentStoreDir();
50
+ await mkdir(dir, { recursive: true });
51
+ const files = (await readdir(dir)).filter((file) => file.endsWith(".json"));
52
+ const items = [];
53
+
54
+ for (const file of files) {
55
+ const filePath = join(dir, file);
56
+ try {
57
+ const result = JSON.parse(await readFile(filePath, "utf8"));
58
+ items.push(summaryFromResult(result, filePath));
59
+ } catch {
60
+ continue;
61
+ }
62
+ }
63
+
64
+ return items.sort((left, right) => right.completed_at.localeCompare(left.completed_at));
65
+ }
66
+
67
+ export async function readAssessmentFromStore(id) {
68
+ const items = await listAssessmentStore();
69
+ const item = items.find((entry) => entry.id === id);
70
+ if (!item) throw new Error(`Assessment not found: ${id}`);
71
+ return JSON.parse(await readFile(item.file_path, "utf8"));
72
+ }
73
+
74
+ export async function compareStoredAssessments(previousId, currentId) {
75
+ const previous = await readAssessmentFromStore(previousId);
76
+ const current = await readAssessmentFromStore(currentId);
77
+ return compareAssessmentResults(previous, current);
78
+ }
@@ -0,0 +1,73 @@
1
+ function requiredPermissions(signal) {
2
+ return (signal.evidence_required || [])
3
+ .filter((item) => item.startsWith("permission:"))
4
+ .map((item) => item.slice("permission:".length));
5
+ }
6
+
7
+ function signalApplies(signal, finding) {
8
+ if (signal.source_type !== "static-analysis") return false;
9
+
10
+ const permissions = requiredPermissions(signal);
11
+ if (permissions.length && !permissions.some((permission) => finding.permissions.includes(permission))) return false;
12
+
13
+ return permissions.length > 0;
14
+ }
15
+
16
+ function signalEvidence(finding) {
17
+ return {
18
+ finding_id: finding.id,
19
+ rule_id: finding.rule_id,
20
+ path: finding.evidence.path,
21
+ line: finding.evidence.line,
22
+ preview: finding.evidence.preview
23
+ };
24
+ }
25
+
26
+ export function deriveTrustSignals({ findings, taxonomies }) {
27
+ const signals = taxonomies.flatMap((taxonomy) => taxonomy.signals || []);
28
+ const emitted = [];
29
+ const seen = new Set();
30
+
31
+ for (const finding of findings) {
32
+ for (const signal of signals) {
33
+ if (!signalApplies(signal, finding)) continue;
34
+
35
+ const key = `${signal.id}:${finding.id}`;
36
+ if (seen.has(key)) continue;
37
+ seen.add(key);
38
+
39
+ emitted.push({
40
+ id: `${signal.id}:${finding.id}`,
41
+ signal_id: signal.id,
42
+ title: signal.title,
43
+ direction: signal.direction,
44
+ weight: signal.weight,
45
+ source_type: signal.source_type,
46
+ applies_to: signal.applies_to,
47
+ description: signal.description,
48
+ evidence: signalEvidence(finding)
49
+ });
50
+ }
51
+ }
52
+
53
+ return emitted;
54
+ }
55
+
56
+ export function summarizeTrustSignals(signals) {
57
+ const byDirection = {};
58
+ const bySource = {};
59
+ let netWeight = 0;
60
+
61
+ for (const signal of signals) {
62
+ byDirection[signal.direction] = (byDirection[signal.direction] || 0) + 1;
63
+ bySource[signal.source_type] = (bySource[signal.source_type] || 0) + 1;
64
+ netWeight += signal.weight;
65
+ }
66
+
67
+ return {
68
+ total: signals.length,
69
+ by_direction: byDirection,
70
+ by_source: bySource,
71
+ net_weight: netWeight
72
+ };
73
+ }
@@ -0,0 +1,17 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const ROOT = join(__dirname, "..", "..");
7
+
8
+ const TRUST_SIGNAL_FILES = ["data/trust/signal-taxonomy.json"];
9
+
10
+ export async function loadTrustSignalTaxonomies() {
11
+ const taxonomies = [];
12
+ for (const file of TRUST_SIGNAL_FILES) {
13
+ const content = await readFile(join(ROOT, file), "utf8");
14
+ taxonomies.push(JSON.parse(content));
15
+ }
16
+ return taxonomies;
17
+ }