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,157 @@
1
+ const OBSERVATION_RULES = {
2
+ "mcp-stdio-process": {
3
+ id: "obs-mcp-stdio-process",
4
+ title: "MCP stdio process configured",
5
+ category: "execution-risk",
6
+ severity: "high",
7
+ confidence: 0.9,
8
+ permissions: ["subprocess-spawn", "mcp-tool-access"],
9
+ why_it_matters: "This MCP configuration starts a local process. The agent can delegate tool calls to that process.",
10
+ recommended_actions: [
11
+ "Verify the MCP command and package source.",
12
+ "Pin package versions instead of using moving tags."
13
+ ],
14
+ recommended_alternatives: [
15
+ "Use a verified local MCP server with a pinned version."
16
+ ],
17
+ migration_instruction: "Replace moving MCP commands with pinned package versions and disable unused MCP servers.",
18
+ supersedes: ["mcp-stdio-process-server"]
19
+ },
20
+ "mcp-remote-endpoint": {
21
+ id: "obs-mcp-remote-endpoint",
22
+ title: "Remote MCP endpoint configured",
23
+ category: "remote-access-risk",
24
+ severity: "medium",
25
+ confidence: 0.86,
26
+ permissions: ["network-access", "external-endpoint", "mcp-tool-access"],
27
+ why_it_matters: "Remote MCP endpoints can provide tools or receive delegated tool calls from the agent environment.",
28
+ recommended_actions: [
29
+ "Verify the remote MCP owner.",
30
+ "Restrict exposed tools if filtering is supported."
31
+ ],
32
+ recommended_alternatives: [
33
+ "Use a local pinned MCP server for sensitive workspaces."
34
+ ],
35
+ migration_instruction: "Disable remote MCP endpoints for sensitive workspaces or replace them with local pinned MCP servers.",
36
+ supersedes: ["mcp-remote-endpoint"]
37
+ },
38
+ "mcp-filesystem-capability": {
39
+ id: "obs-mcp-filesystem-capability",
40
+ title: "Filesystem MCP capability configured",
41
+ category: "data-exposure-risk",
42
+ severity: "high",
43
+ confidence: 0.88,
44
+ permissions: ["filesystem-read", "filesystem-write", "mcp-tool-access"],
45
+ why_it_matters: "Filesystem MCP tools can allow the agent to read or modify local files through tool calls.",
46
+ recommended_actions: [
47
+ "Limit filesystem access to a dedicated workspace.",
48
+ "Disable write-capable filesystem tools unless required."
49
+ ],
50
+ recommended_alternatives: [
51
+ "Use read-only filesystem tooling for inspection tasks."
52
+ ],
53
+ migration_instruction: "Replace broad filesystem MCP access with workspace-scoped or read-only access.",
54
+ supersedes: ["mcp-filesystem-write"]
55
+ },
56
+ "remote-trigger-config": {
57
+ id: "obs-remote-trigger-config",
58
+ title: "Remote trigger channel configured",
59
+ category: "remote-access-risk",
60
+ severity: "medium",
61
+ confidence: 0.84,
62
+ permissions: ["remote-trigger", "credential-access"],
63
+ why_it_matters: "Remote trigger channels can start or influence agent sessions. Policies and allowlists determine who can reach the agent.",
64
+ recommended_actions: [
65
+ "Set explicit allowlists for remote users or groups.",
66
+ "Disable remote channels for sensitive workspaces."
67
+ ],
68
+ recommended_alternatives: [
69
+ "Use local-only mode for sensitive workflows."
70
+ ],
71
+ migration_instruction: "Set explicit allowFrom values and disable open group triggers.",
72
+ supersedes: ["openclaw-remote-channel-policy", "hermes-gateway-trigger"]
73
+ },
74
+ "scheduled-agent-execution": {
75
+ id: "obs-scheduled-agent-execution",
76
+ title: "Scheduled agent execution configured",
77
+ category: "persistence-automation-risk",
78
+ severity: "medium",
79
+ confidence: 0.84,
80
+ permissions: ["scheduled-execution"],
81
+ why_it_matters: "Scheduled tasks can start agent behavior later, when the user is not actively reviewing each action.",
82
+ recommended_actions: [
83
+ "Review scheduled task prompts and allowed tools.",
84
+ "Disable unused scheduled tasks."
85
+ ],
86
+ recommended_alternatives: [
87
+ "Use manual run mode for sensitive workflows."
88
+ ],
89
+ migration_instruction: "Disable scheduled execution until task prompts and tool permissions are reviewed.",
90
+ supersedes: ["openclaw-scheduled-task"]
91
+ },
92
+ "credential-reference": {
93
+ id: "obs-credential-reference",
94
+ title: "Credential reference in structured config",
95
+ category: "data-exposure-risk",
96
+ severity: "medium",
97
+ confidence: 0.82,
98
+ permissions: ["credential-access", "env-read"],
99
+ why_it_matters: "Structured config references credentials that may be inherited by tools, skills or MCP servers.",
100
+ recommended_actions: [
101
+ "Use scoped credentials for agent environments.",
102
+ "Remove unused secrets before running the agent."
103
+ ],
104
+ recommended_alternatives: [
105
+ "Use a dedicated low-privilege token for this agent."
106
+ ],
107
+ migration_instruction: "Move high-privilege secrets out of the agent environment and use scoped replacement tokens.",
108
+ supersedes: ["core-env-reference"]
109
+ }
110
+ };
111
+
112
+ function previewValue(value) {
113
+ if (typeof value === "string") return value;
114
+ return JSON.stringify(value).slice(0, 240);
115
+ }
116
+
117
+ export function findingsFromObservations({ observations, profileIds }) {
118
+ const seen = new Set();
119
+ const findings = [];
120
+
121
+ for (const observation of observations) {
122
+ const rule = OBSERVATION_RULES[observation.type];
123
+ const key = `${rule.id}:${observation.path}:${observation.json_path}`;
124
+ if (seen.has(key)) continue;
125
+ seen.add(key);
126
+
127
+ findings.push({
128
+ id: `${rule.id}:${observation.path}:${observation.json_path}`,
129
+ rule_id: rule.id,
130
+ title: rule.title,
131
+ category: rule.category,
132
+ severity: rule.severity,
133
+ confidence: rule.confidence,
134
+ permissions: rule.permissions,
135
+ profile_ids: profileIds,
136
+ why_it_matters: rule.why_it_matters,
137
+ recommended_actions: rule.recommended_actions,
138
+ recommended_alternatives: rule.recommended_alternatives,
139
+ migration_instruction: rule.migration_instruction,
140
+ supersedes: rule.supersedes || [],
141
+ evidence: {
142
+ path: observation.path,
143
+ line: observation.line || 1,
144
+ preview: `${observation.json_path}: ${previewValue(observation.value)}`
145
+ },
146
+ evidence_items: [
147
+ {
148
+ path: observation.path,
149
+ line: observation.line || 1,
150
+ preview: `${observation.json_path}: ${previewValue(observation.value)}`
151
+ }
152
+ ]
153
+ });
154
+ }
155
+
156
+ return findings;
157
+ }
@@ -0,0 +1,130 @@
1
+ import { access, 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 PROFILE_FILES = [
9
+ "profiles/generic-agent/profile.json",
10
+ "profiles/openclaw-like/profile.json",
11
+ "profiles/hermes-like/profile.json",
12
+ "profiles/mcp-server/profile.json",
13
+ "profiles/skill-runtime/profile.json"
14
+ ];
15
+
16
+ export async function loadProfiles() {
17
+ const profiles = [];
18
+ for (const file of PROFILE_FILES) {
19
+ const content = await readFile(join(ROOT, file), "utf8");
20
+ profiles.push(JSON.parse(content));
21
+ }
22
+ return profiles;
23
+ }
24
+
25
+ async function exists(path) {
26
+ try {
27
+ await access(path);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ async function detectProfile(root) {
35
+ const lowerRoot = root.toLowerCase();
36
+ if (lowerRoot.includes(".openclaw")) {
37
+ return {
38
+ profileId: "openclaw-like",
39
+ signals: [{ type: "path-name", value: ".openclaw" }]
40
+ };
41
+ }
42
+ if (lowerRoot.includes(".hermes")) {
43
+ return {
44
+ profileId: "hermes-like",
45
+ signals: [{ type: "path-name", value: ".hermes" }]
46
+ };
47
+ }
48
+
49
+ const hermesHints = [
50
+ ".hermes",
51
+ ".hermes/config.json",
52
+ ".hermes/config.yaml",
53
+ "optional-mcps",
54
+ "optional-skills",
55
+ "gateway",
56
+ "cron",
57
+ "memory",
58
+ "skills/openclaw-imports"
59
+ ];
60
+ for (const hint of hermesHints) {
61
+ if (await exists(join(root, hint))) {
62
+ return {
63
+ profileId: "hermes-like",
64
+ signals: [{ type: "path-exists", value: hint }]
65
+ };
66
+ }
67
+ }
68
+
69
+ const openclawHints = [
70
+ "openclaw.json",
71
+ ".openclaw/openclaw.json",
72
+ "SOUL.md",
73
+ "TOOLS.md",
74
+ "workspace/skills"
75
+ ];
76
+ for (const hint of openclawHints) {
77
+ if (await exists(join(root, hint))) {
78
+ return {
79
+ profileId: "openclaw-like",
80
+ signals: [{ type: "path-exists", value: hint }]
81
+ };
82
+ }
83
+ }
84
+
85
+ return {
86
+ profileId: "generic-agent",
87
+ signals: []
88
+ };
89
+ }
90
+
91
+ function expandProfile(profiles, profileId) {
92
+ const profile = profiles.find((item) => item.id === profileId);
93
+ if (!profile) {
94
+ throw new Error(`Unknown profile: ${profileId}`);
95
+ }
96
+ const expanded = [profile];
97
+ for (const parentId of profile.extends || []) {
98
+ const parent = profiles.find((item) => item.id === parentId);
99
+ if (parent && !expanded.some((item) => item.id === parent.id)) {
100
+ expanded.push(parent);
101
+ }
102
+ }
103
+ return expanded;
104
+ }
105
+
106
+ export async function resolveProfileSelection({ profiles, requestedProfile, root }) {
107
+ if (requestedProfile) {
108
+ return {
109
+ mode: "requested",
110
+ requested_profile: requestedProfile,
111
+ selected_profile: requestedProfile,
112
+ detection_signals: [{ type: "cli-profile", value: requestedProfile }],
113
+ profiles: expandProfile(profiles, requestedProfile)
114
+ };
115
+ }
116
+
117
+ const detected = await detectProfile(root);
118
+ return {
119
+ mode: "autodetected",
120
+ requested_profile: null,
121
+ selected_profile: detected.profileId,
122
+ detection_signals: detected.signals,
123
+ profiles: expandProfile(profiles, detected.profileId)
124
+ };
125
+ }
126
+
127
+ export async function selectProfiles({ profiles, requestedProfile, root }) {
128
+ const selection = await resolveProfileSelection({ profiles, requestedProfile, root });
129
+ return selection.profiles;
130
+ }
@@ -0,0 +1,94 @@
1
+ function activeAt(edge, at) {
2
+ const timestamp = new Date(at).getTime();
3
+ const starts = new Date(edge.valid_from).getTime();
4
+ const ends = edge.valid_until ? new Date(edge.valid_until).getTime() : Number.POSITIVE_INFINITY;
5
+ return Number.isFinite(timestamp) && timestamp >= starts && timestamp <= ends;
6
+ }
7
+
8
+ function indexedComponents(components = []) {
9
+ return new Map(components.map((component) => [component.id, component]));
10
+ }
11
+
12
+ function isStrictReviewed(component) {
13
+ return (
14
+ component?.intelligence_state === "strict_reviewed"
15
+ && component?.publication_gate?.strict_pass === true
16
+ );
17
+ }
18
+
19
+ export function findComponentAlternatives({
20
+ componentId,
21
+ graph = {},
22
+ components = [],
23
+ at = new Date().toISOString(),
24
+ includePeers = false
25
+ }) {
26
+ if (!componentId) return [];
27
+ const policy = graph.policy || {};
28
+ const allowedRelationships = new Set(policy.agent_facing_relationships || []);
29
+ const minimumConfidence = Number(policy.minimum_confidence ?? 1);
30
+ const minimumSafetyDelta = Number(policy.minimum_safety_delta ?? 0);
31
+ const componentIndex = indexedComponents(components);
32
+ const source = componentIndex.get(componentId);
33
+
34
+ return (graph.edges || [])
35
+ .filter((edge) => edge.source_component_id === componentId)
36
+ .filter((edge) => edge.status === "active" && activeAt(edge, at))
37
+ .filter((edge) => includePeers || allowedRelationships.has(edge.relationship_type))
38
+ .filter((edge) => includePeers || edge.confidence >= minimumConfidence)
39
+ .filter((edge) => includePeers || edge.safety_delta >= minimumSafetyDelta)
40
+ .map((edge) => {
41
+ const target = componentIndex.get(edge.target_component_id);
42
+ return { edge, source, target };
43
+ })
44
+ .filter(({ source: sourceComponent, target }) =>
45
+ isStrictReviewed(sourceComponent)
46
+ && isStrictReviewed(target)
47
+ && sourceComponent.type === target.type
48
+ )
49
+ .sort((left, right) =>
50
+ right.edge.safety_delta - left.edge.safety_delta
51
+ || right.edge.confidence - left.edge.confidence
52
+ || Number(right.target.trust_score || 0) - Number(left.target.trust_score || 0)
53
+ )
54
+ .map(({ edge, target }) => ({
55
+ id: edge.id,
56
+ component_id: target.id,
57
+ name: target.name,
58
+ component_type: target.type,
59
+ relationship_type: edge.relationship_type,
60
+ confidence: edge.confidence,
61
+ safety_delta: edge.safety_delta,
62
+ trust_score: target.trust_score,
63
+ decision: target.decision,
64
+ reason: edge.reason,
65
+ shared_capabilities: edge.shared_capabilities,
66
+ unsupported_source_capabilities: edge.unsupported_source_capabilities,
67
+ conditions: edge.conditions,
68
+ migration: edge.migration,
69
+ evidence: edge.evidence,
70
+ valid_from: edge.valid_from,
71
+ valid_until: edge.valid_until
72
+ }));
73
+ }
74
+
75
+ export function alternativeCoverageFor({ componentId, graph = {}, alternatives = [] }) {
76
+ const gap = (graph.coverage_gaps || []).find((item) => item.component_id === componentId);
77
+ if (alternatives.length) {
78
+ return {
79
+ status: alternatives.some((item) => item.relationship_type === "verified_alternative")
80
+ ? "verified"
81
+ : "conditional",
82
+ reviewed_alternative_count: alternatives.length,
83
+ reason: null,
84
+ graph_version: graph.version || null
85
+ };
86
+ }
87
+ return {
88
+ status: "gap",
89
+ reviewed_alternative_count: 0,
90
+ reason: gap?.reason || "No evidence-backed safer functional alternative is currently available.",
91
+ target_research_queries: gap?.target_research_queries || [],
92
+ graph_version: graph.version || null
93
+ };
94
+ }
@@ -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 RECOMMENDATION_PACK_FILES = ["data/recommendations/core/recommendations.json"];
9
+
10
+ export async function loadRecommendationPacks() {
11
+ const packs = [];
12
+ for (const file of RECOMMENDATION_PACK_FILES) {
13
+ const content = await readFile(join(ROOT, file), "utf8");
14
+ packs.push(JSON.parse(content));
15
+ }
16
+ return packs;
17
+ }
@@ -0,0 +1,79 @@
1
+ function intersects(left = [], right = []) {
2
+ return left.some((item) => right.includes(item));
3
+ }
4
+
5
+ function includesAll(left = [], right = []) {
6
+ return right.every((item) => left.includes(item));
7
+ }
8
+
9
+ function appliesToFinding(recommendation, finding) {
10
+ const appliesTo = recommendation.applies_to || {};
11
+
12
+ if (appliesTo.rule_ids?.length && !appliesTo.rule_ids.includes(finding.rule_id)) {
13
+ return false;
14
+ }
15
+ if (appliesTo.categories?.length && !appliesTo.categories.includes(finding.category)) {
16
+ return false;
17
+ }
18
+ if (appliesTo.permissions_any?.length && !intersects(finding.permissions, appliesTo.permissions_any)) {
19
+ return false;
20
+ }
21
+ if (appliesTo.permissions_all?.length && !includesAll(finding.permissions, appliesTo.permissions_all)) {
22
+ return false;
23
+ }
24
+ if (appliesTo.profile_ids?.length && !intersects(finding.profile_ids, appliesTo.profile_ids)) {
25
+ return false;
26
+ }
27
+
28
+ return true;
29
+ }
30
+
31
+ function publicRecommendation(recommendation) {
32
+ return {
33
+ id: recommendation.id,
34
+ title: recommendation.title,
35
+ type: recommendation.type,
36
+ status: recommendation.status,
37
+ source: recommendation.source,
38
+ confidence: recommendation.confidence,
39
+ rank: recommendation.rank,
40
+ recommended_actions: recommendation.recommended_actions,
41
+ recommended_alternatives: recommendation.recommended_alternatives,
42
+ agent_instruction: recommendation.agent_instruction,
43
+ one_step_commands: recommendation.one_step_commands || [],
44
+ rollback_note: recommendation.rollback_note
45
+ };
46
+ }
47
+
48
+ function unique(values) {
49
+ return [...new Set(values.filter(Boolean))];
50
+ }
51
+
52
+ export function applyRecommendations(findings, recommendationPacks) {
53
+ const recommendations = recommendationPacks.flatMap((pack) => pack.recommendations || []);
54
+
55
+ return findings.map((finding) => {
56
+ const matched = recommendations
57
+ .filter((recommendation) => appliesToFinding(recommendation, finding))
58
+ .sort((left, right) => right.rank - left.rank || right.confidence - left.confidence)
59
+ .map(publicRecommendation);
60
+
61
+ const topRecommendation = matched[0];
62
+ const recommendedActions = unique([
63
+ ...(topRecommendation?.recommended_actions || []),
64
+ ...(finding.recommended_actions || [])
65
+ ]);
66
+ const recommendedAlternatives = unique([
67
+ ...(topRecommendation?.recommended_alternatives || []),
68
+ ...(finding.recommended_alternatives || [])
69
+ ]);
70
+
71
+ return {
72
+ ...finding,
73
+ recommended_actions: recommendedActions,
74
+ recommended_alternatives: recommendedAlternatives,
75
+ migration_instruction: topRecommendation?.agent_instruction || finding.migration_instruction || "",
76
+ recommendations: matched
77
+ };
78
+ });
79
+ }
@@ -0,0 +1,71 @@
1
+ export function renderComparisonConsole(result) {
2
+ const lines = [];
3
+ const delta = result.score.delta >= 0 ? `+${result.score.delta}` : String(result.score.delta);
4
+
5
+ lines.push("AgentSecurityLens Assessment Comparison");
6
+ lines.push("");
7
+ lines.push(`Previous: ${result.comparison.previous_assessment_id}`);
8
+ lines.push(`Current: ${result.comparison.current_assessment_id}`);
9
+ lines.push(`Trust Score: ${result.score.previous} -> ${result.score.current} (${delta})`);
10
+ lines.push(
11
+ `Findings: ${result.finding_counts.previous} -> ${result.finding_counts.current} ` +
12
+ `(added ${result.finding_counts.added}, resolved ${result.finding_counts.resolved}, persistent ${result.finding_counts.persistent})`
13
+ );
14
+ lines.push("");
15
+
16
+ if (result.profiles.added.length || result.profiles.removed.length || result.profiles.changed.length) {
17
+ lines.push("Profile Changes:");
18
+ for (const item of result.profiles.added) lines.push(`- Added ${item.id}@${item.version}`);
19
+ for (const item of result.profiles.removed) lines.push(`- Removed ${item.id}@${item.version}`);
20
+ for (const item of result.profiles.changed) {
21
+ lines.push(`- Changed ${item.id}: ${item.previous_version} -> ${item.current_version}`);
22
+ }
23
+ lines.push("");
24
+ }
25
+
26
+ if (result.rule_packs.added.length || result.rule_packs.removed.length || result.rule_packs.changed.length) {
27
+ lines.push("Rule Pack Changes:");
28
+ for (const item of result.rule_packs.added) lines.push(`- Added ${item.id}@${item.version}`);
29
+ for (const item of result.rule_packs.removed) lines.push(`- Removed ${item.id}@${item.version}`);
30
+ for (const item of result.rule_packs.changed) {
31
+ lines.push(`- Changed ${item.id}: ${item.previous_version} -> ${item.current_version}`);
32
+ }
33
+ lines.push("");
34
+ }
35
+
36
+ if (
37
+ result.recommendation_packs.added.length ||
38
+ result.recommendation_packs.removed.length ||
39
+ result.recommendation_packs.changed.length
40
+ ) {
41
+ lines.push("Recommendation Pack Changes:");
42
+ for (const item of result.recommendation_packs.added) lines.push(`- Added ${item.id}@${item.version}`);
43
+ for (const item of result.recommendation_packs.removed) lines.push(`- Removed ${item.id}@${item.version}`);
44
+ for (const item of result.recommendation_packs.changed) {
45
+ lines.push(`- Changed ${item.id}: ${item.previous_version} -> ${item.current_version}`);
46
+ }
47
+ lines.push("");
48
+ }
49
+
50
+ if (result.findings.added.length) {
51
+ lines.push("Added Findings:");
52
+ for (const finding of result.findings.added) {
53
+ lines.push(`- [${finding.severity}] ${finding.title} (${finding.evidence.path}:${finding.evidence.line})`);
54
+ }
55
+ lines.push("");
56
+ }
57
+
58
+ if (result.findings.resolved.length) {
59
+ lines.push("Resolved Findings:");
60
+ for (const finding of result.findings.resolved) {
61
+ lines.push(`- [${finding.severity}] ${finding.title} (${finding.evidence.path}:${finding.evidence.line})`);
62
+ }
63
+ lines.push("");
64
+ }
65
+
66
+ if (!result.findings.added.length && !result.findings.resolved.length) {
67
+ lines.push("No finding delta detected.");
68
+ }
69
+
70
+ return lines.join("\n");
71
+ }
@@ -0,0 +1,103 @@
1
+ export function renderConsole(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
+ for (const profile of result.profiles) {
14
+ lines.push(`- ${profile.id}@${profile.version} (${profile.status}, confidence ${profile.confidence})`);
15
+ }
16
+ lines.push("");
17
+ lines.push("Rule Packs:");
18
+ for (const pack of result.rule_packs) {
19
+ lines.push(`- ${pack.id}@${pack.version} (${pack.rule_count} rules)`);
20
+ }
21
+ lines.push("");
22
+ if (result.recommendation_packs?.length) {
23
+ lines.push("Recommendation Packs:");
24
+ for (const pack of result.recommendation_packs) {
25
+ lines.push(`- ${pack.id}@${pack.version} (${pack.status}, ${pack.recommendation_count} recommendations)`);
26
+ }
27
+ lines.push("");
28
+ }
29
+ if (result.trust_signal_taxonomies?.length) {
30
+ lines.push("Trust Signal Taxonomies:");
31
+ for (const taxonomy of result.trust_signal_taxonomies) {
32
+ lines.push(`- ${taxonomy.id}@${taxonomy.version} (${taxonomy.status}, ${taxonomy.signal_count} signals)`);
33
+ }
34
+ lines.push("");
35
+ }
36
+ lines.push("Profile Selection:");
37
+ lines.push(`- Mode: ${result.lineage.profile_selection.mode}`);
38
+ lines.push(`- Selected: ${result.lineage.profile_selection.selected_profile}`);
39
+ if (result.lineage.profile_selection.detection_signals.length) {
40
+ const signals = result.lineage.profile_selection.detection_signals
41
+ .map((signal) => `${signal.type}:${signal.value}`)
42
+ .join(", ");
43
+ lines.push(`- Signals: ${signals}`);
44
+ }
45
+ lines.push("");
46
+
47
+ if (result.risk_domains?.length) {
48
+ lines.push("Risk Domains:");
49
+ for (const domain of result.risk_domains) {
50
+ lines.push(`- ${domain.title}: ${domain.count} finding(s), highest=${domain.highest_severity}`);
51
+ }
52
+ lines.push("");
53
+ }
54
+
55
+ if (result.trust_signals?.length) {
56
+ const summary = result.summary.trust_signal_summary;
57
+ lines.push("Trust Signals:");
58
+ lines.push(`- Total: ${summary.total}, net weight=${summary.net_weight}`);
59
+ lines.push(
60
+ `- Direction: ${Object.entries(summary.by_direction).map(([key, value]) => `${key}=${value}`).join(", ")}`
61
+ );
62
+ for (const signal of result.trust_signals.slice(0, 5)) {
63
+ lines.push(`- ${signal.title} (${signal.signal_id}, ${signal.weight}) at ${signal.evidence.path}:${signal.evidence.line}`);
64
+ }
65
+ lines.push("");
66
+ }
67
+
68
+ if (result.findings.length === 0) {
69
+ lines.push("No risk findings detected by the current rule packs.");
70
+ lines.push("Known limitation: this does not prove the agent environment is safe.");
71
+ return lines.join("\n");
72
+ }
73
+
74
+ lines.push("Detailed Findings:");
75
+ for (const finding of result.findings) {
76
+ lines.push("");
77
+ lines.push(`[${finding.severity.toUpperCase()}] ${finding.title}`);
78
+ lines.push(`Category: ${finding.category}`);
79
+ lines.push(`Evidence: ${finding.evidence.path}:${finding.evidence.line}`);
80
+ if (finding.evidence_items.length > 1) {
81
+ lines.push(`Additional evidence: ${finding.evidence_items.length - 1} more match(es) in this file`);
82
+ }
83
+ lines.push(`Why it matters: ${finding.why_it_matters}`);
84
+ if (finding.recommended_actions.length) {
85
+ lines.push(`Actions: ${finding.recommended_actions.join("; ")}`);
86
+ }
87
+ if (finding.recommended_alternatives.length) {
88
+ lines.push(`Alternatives: ${finding.recommended_alternatives.join("; ")}`);
89
+ }
90
+ if (finding.migration_instruction) {
91
+ lines.push(`Migration: ${finding.migration_instruction}`);
92
+ }
93
+ if (finding.recommendations?.length) {
94
+ const top = finding.recommendations[0];
95
+ lines.push(`Top recommendation: ${top.title} (${top.id}, confidence ${top.confidence})`);
96
+ if (top.one_step_commands?.length) {
97
+ lines.push(`One-step instruction: ${top.one_step_commands[0].command}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ return lines.join("\n");
103
+ }