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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export const EXPOSURE_CONTEXT_WEIGHTS = {
|
|
2
|
+
runtime_exposure: 1,
|
|
3
|
+
install_exposure: 0.85,
|
|
4
|
+
supply_chain_exposure: 0.35,
|
|
5
|
+
documented_optional_capability: 0.45,
|
|
6
|
+
repository_maintenance_activity: 0
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const MANIFEST_NAMES = new Set([
|
|
10
|
+
"package.json",
|
|
11
|
+
"package-lock.json",
|
|
12
|
+
"pnpm-lock.yaml",
|
|
13
|
+
"yarn.lock",
|
|
14
|
+
"pyproject.toml",
|
|
15
|
+
"requirements.txt",
|
|
16
|
+
"poetry.lock",
|
|
17
|
+
"uv.lock",
|
|
18
|
+
"server.json",
|
|
19
|
+
"mcp.json",
|
|
20
|
+
"mcp.yaml",
|
|
21
|
+
"mcp.yml",
|
|
22
|
+
"manifest.json"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function normalizedPath(path = "") {
|
|
26
|
+
return String(path).replaceAll("\\", "/").toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileName(path = "") {
|
|
30
|
+
return normalizedPath(path).split("/").at(-1) || "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isDocumentation(path) {
|
|
34
|
+
return /\.(?:md|mdx|rst|txt)$/i.test(path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInstallPath(path) {
|
|
38
|
+
return /(?:^|\/)(?:install|setup|bootstrap|entrypoint)(?:[._-]|\/|$)/i.test(path)
|
|
39
|
+
|| /(?:install|setup|bootstrap)\.(?:sh|ps1|py|js|mjs|cjs|ts)$/i.test(path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isMaintenancePath(path) {
|
|
43
|
+
return /(?:^|\/)\.github\/(?:workflows|actions)\//i.test(path)
|
|
44
|
+
|| /(?:^|\/)(?:release|publish)(?:[._-]|\/)/i.test(path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function actionableInstallLine(line = "") {
|
|
48
|
+
return /\b(?:curl|wget|invoke-webrequest)\b[^\r\n|;]*(?:\||;).*\b(?:sh|bash|zsh|powershell|python|node)\b/i.test(line)
|
|
49
|
+
|| /\b(?:npx\s+-y|uvx|pipx\s+run|pip\s+install|npm\s+install|pnpm\s+add|yarn\s+add)\b/i.test(line);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function classifyFindingContext({ path = "", line = "", ruleId = "", signal = "" } = {}) {
|
|
53
|
+
const normalized = normalizedPath(path);
|
|
54
|
+
const name = fileName(path);
|
|
55
|
+
|
|
56
|
+
if (isMaintenancePath(normalized)) {
|
|
57
|
+
return {
|
|
58
|
+
context: "repository_maintenance_activity",
|
|
59
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.repository_maintenance_activity,
|
|
60
|
+
rationale: "The finding is in repository CI, release or publishing automation rather than installed component runtime."
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isInstallPath(normalized)) {
|
|
65
|
+
return {
|
|
66
|
+
context: "install_exposure",
|
|
67
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.install_exposure,
|
|
68
|
+
rationale: "The finding is in an installation, setup or bootstrap entrypoint."
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (/(?:^|\/)(?:examples?|tests?|test|fixtures?|docs?\/scripts)\//i.test(normalized)
|
|
73
|
+
|| /(?:^|\/)\.github\/skills?\//i.test(normalized)) {
|
|
74
|
+
return {
|
|
75
|
+
context: "documented_optional_capability",
|
|
76
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.documented_optional_capability,
|
|
77
|
+
rationale: "The finding is in an example, test, fixture or optional bundled Skill rather than the default runtime path."
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (MANIFEST_NAMES.has(name)) {
|
|
82
|
+
const installSurface =
|
|
83
|
+
signal === "remote-code-install"
|
|
84
|
+
|| /\b(?:runtimehint|bin|postinstall|preinstall|install)\b/i.test(line);
|
|
85
|
+
return {
|
|
86
|
+
context: installSurface ? "install_exposure" : "supply_chain_exposure",
|
|
87
|
+
weight: installSurface
|
|
88
|
+
? EXPOSURE_CONTEXT_WEIGHTS.install_exposure
|
|
89
|
+
: EXPOSURE_CONTEXT_WEIGHTS.supply_chain_exposure,
|
|
90
|
+
rationale: installSurface
|
|
91
|
+
? "The manifest declares an installation or executable entrypoint."
|
|
92
|
+
: "The finding is dependency or package metadata and does not by itself prove runtime access."
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isDocumentation(normalized)) {
|
|
97
|
+
if (ruleId === "repo-instruction-override" || signal === "prompt-injection-pattern") {
|
|
98
|
+
return {
|
|
99
|
+
context: "runtime_exposure",
|
|
100
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.runtime_exposure,
|
|
101
|
+
rationale: "Instruction override text is consumed by an Agent at runtime."
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (actionableInstallLine(line) || signal === "remote-code-install") {
|
|
105
|
+
return {
|
|
106
|
+
context: "install_exposure",
|
|
107
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.install_exposure,
|
|
108
|
+
rationale: "The documentation contains an actionable install command."
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
context: "documented_optional_capability",
|
|
113
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.documented_optional_capability,
|
|
114
|
+
rationale: "The documentation declares a capability or example that may not be enabled in every run."
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (/^(?:dockerfile|docker-compose\.ya?ml)$/i.test(name)) {
|
|
119
|
+
return {
|
|
120
|
+
context: "install_exposure",
|
|
121
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.install_exposure,
|
|
122
|
+
rationale: "The finding is part of the component build or deployment surface."
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
context: "runtime_exposure",
|
|
128
|
+
weight: EXPOSURE_CONTEXT_WEIGHTS.runtime_exposure,
|
|
129
|
+
rationale: "The finding is in executable component source or runtime configuration."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function contextualizeFindings(findings = []) {
|
|
134
|
+
return findings.map((finding) => {
|
|
135
|
+
const classification = classifyFindingContext({
|
|
136
|
+
path: finding.evidence?.path,
|
|
137
|
+
line: finding.evidence?.preview,
|
|
138
|
+
ruleId: finding.rule_id,
|
|
139
|
+
signal: finding.risk_signal
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
...finding,
|
|
143
|
+
exposure_context: classification.context,
|
|
144
|
+
context_weight: classification.weight,
|
|
145
|
+
context_rationale: classification.rationale,
|
|
146
|
+
effective_confidence: Number((Number(finding.confidence || 0) * classification.weight).toFixed(3))
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function summarizeFindingContexts(findings = []) {
|
|
152
|
+
const contexts = {};
|
|
153
|
+
const signalContexts = {};
|
|
154
|
+
for (const finding of findings) {
|
|
155
|
+
const context = finding.exposure_context || "runtime_exposure";
|
|
156
|
+
contexts[context] ||= { finding_count: 0, signals: [] };
|
|
157
|
+
contexts[context].finding_count += 1;
|
|
158
|
+
contexts[context].signals.push(finding.risk_signal);
|
|
159
|
+
signalContexts[finding.risk_signal] ||= [];
|
|
160
|
+
signalContexts[finding.risk_signal].push(context);
|
|
161
|
+
}
|
|
162
|
+
for (const item of Object.values(contexts)) item.signals = [...new Set(item.signals)];
|
|
163
|
+
for (const [signal, values] of Object.entries(signalContexts)) {
|
|
164
|
+
signalContexts[signal] = [...new Set(values)];
|
|
165
|
+
}
|
|
166
|
+
const effectiveFindings = findings.filter((finding) =>
|
|
167
|
+
!["repository_maintenance_activity", "supply_chain_exposure"].includes(finding.exposure_context)
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
contexts,
|
|
171
|
+
signal_contexts: signalContexts,
|
|
172
|
+
observed_signals: [...new Set(findings.map((item) => item.risk_signal))],
|
|
173
|
+
effective_risk_signals: [...new Set(effectiveFindings.map((item) => item.risk_signal))],
|
|
174
|
+
non_runtime_observations: [...new Set(
|
|
175
|
+
findings
|
|
176
|
+
.filter((finding) => ["repository_maintenance_activity", "supply_chain_exposure"].includes(finding.exposure_context))
|
|
177
|
+
.map((item) => item.risk_signal)
|
|
178
|
+
)]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const EXPOSURE_POINTS = {
|
|
2
|
+
"remote-code-install": 24,
|
|
3
|
+
"shell-execution": 20,
|
|
4
|
+
"subprocess-spawn": 20,
|
|
5
|
+
"dynamic-code-execution": 20,
|
|
6
|
+
"credential-access": 18,
|
|
7
|
+
"filesystem-write": 15,
|
|
8
|
+
"filesystem-read": 10,
|
|
9
|
+
"browser-access": 15,
|
|
10
|
+
"database-access": 15,
|
|
11
|
+
"data-retention": 12,
|
|
12
|
+
"network-access": 12,
|
|
13
|
+
"external-api": 12,
|
|
14
|
+
webhook: 12,
|
|
15
|
+
"remote-mcp-endpoint": 12,
|
|
16
|
+
"background-execution": 10,
|
|
17
|
+
"scheduled-trigger": 10,
|
|
18
|
+
"workflow-automation": 10,
|
|
19
|
+
"multi-agent-delegation": 10,
|
|
20
|
+
"tool-chaining": 8,
|
|
21
|
+
"third-party-integration": 8,
|
|
22
|
+
"hidden-instruction": 18,
|
|
23
|
+
"override-rules": 18,
|
|
24
|
+
"ignore-safety": 18,
|
|
25
|
+
"prompt-injection-pattern": 14,
|
|
26
|
+
"repository-write": 14,
|
|
27
|
+
"message-write": 10,
|
|
28
|
+
"unknown-source": 12,
|
|
29
|
+
"catalog-unreviewed": 8
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const CONTROL_POINTS = {
|
|
33
|
+
"sandbox-or-container": 18,
|
|
34
|
+
"workspace-scope": 14,
|
|
35
|
+
"read-only-mode": 14,
|
|
36
|
+
"command-confirmation": 14,
|
|
37
|
+
"scoped-credentials": 14,
|
|
38
|
+
"network-allowlist": 10,
|
|
39
|
+
"isolated-browser-profile": 10,
|
|
40
|
+
"audit-logging": 8,
|
|
41
|
+
"pinned-version": 8,
|
|
42
|
+
"automatic-update-disabled": 6,
|
|
43
|
+
"destructive-action-confirmation": 8
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CRITICAL_INCIDENT_TYPES = new Set([
|
|
47
|
+
"credential_theft",
|
|
48
|
+
"hidden_data_exfiltration",
|
|
49
|
+
"confirmed_malicious_code",
|
|
50
|
+
"unauthorized_remote_execution",
|
|
51
|
+
"backdoor"
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const HARD_FAILURE_SIGNALS = new Set([
|
|
55
|
+
"credential-theft-confirmed",
|
|
56
|
+
"hidden-data-exfiltration",
|
|
57
|
+
"malicious-code-confirmed",
|
|
58
|
+
"token-passthrough",
|
|
59
|
+
"known-critical-vulnerability-unmitigated",
|
|
60
|
+
"version-mismatch",
|
|
61
|
+
"destructive-action-without-confirmation"
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function clamp(value) {
|
|
65
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unique(values = []) {
|
|
69
|
+
return [...new Set(values.filter(Boolean))];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function evidenceGroups(known = {}) {
|
|
73
|
+
return new Map((known?.evidence || []).map((item) => [item.kind, item]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourceRecords(known = {}) {
|
|
77
|
+
return evidenceGroups(known).get("source")?.records || [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function technicalScan(known = {}) {
|
|
81
|
+
return evidenceGroups(known).get("technical_scan")?.scan || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function qualityReview(known = {}) {
|
|
85
|
+
return evidenceGroups(known).get("independent_quality_review")?.review || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasCommunityEvidence(records = []) {
|
|
89
|
+
return records.some((item) => [
|
|
90
|
+
"github_issues_api",
|
|
91
|
+
"github_issues_page",
|
|
92
|
+
"hacker_news_search_api",
|
|
93
|
+
"community_report",
|
|
94
|
+
"community_signal",
|
|
95
|
+
"security_advisory"
|
|
96
|
+
].includes(item.source_type));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function controlsFrom(input = {}, known = null) {
|
|
100
|
+
const explicit = [
|
|
101
|
+
...(input.applied_controls || []),
|
|
102
|
+
...(input.security_controls || []),
|
|
103
|
+
...(known?.verified_controls || [])
|
|
104
|
+
];
|
|
105
|
+
if (input.exact_version || input.commit_sha || input.package_digest) explicit.push("pinned-version");
|
|
106
|
+
if (input.sandboxed === true || input.containerized === true) explicit.push("sandbox-or-container");
|
|
107
|
+
if (input.read_only === true) explicit.push("read-only-mode");
|
|
108
|
+
if (input.user_confirmation_for_commands === true) explicit.push("command-confirmation");
|
|
109
|
+
if (input.scoped_credentials === true) explicit.push("scoped-credentials");
|
|
110
|
+
if (input.network_allowlist === true) explicit.push("network-allowlist");
|
|
111
|
+
if (input.isolated_browser_profile === true) explicit.push("isolated-browser-profile");
|
|
112
|
+
return unique(explicit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function contextWeightForSignal(signal, known = null) {
|
|
116
|
+
const contexts = known?.signal_contexts?.[signal] || [];
|
|
117
|
+
if (!contexts.length) return 1;
|
|
118
|
+
const weights = {
|
|
119
|
+
runtime_exposure: 1,
|
|
120
|
+
install_exposure: 0.85,
|
|
121
|
+
supply_chain_exposure: 0.35,
|
|
122
|
+
documented_optional_capability: 0.45,
|
|
123
|
+
repository_maintenance_activity: 0
|
|
124
|
+
};
|
|
125
|
+
return Math.max(...contexts.map((context) => weights[context] ?? 1));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exposureDimension(risks = [], known = null) {
|
|
129
|
+
const applied = unique(risks).map((signal) => ({
|
|
130
|
+
signal,
|
|
131
|
+
context_weight: contextWeightForSignal(signal, known),
|
|
132
|
+
points: Math.round((EXPOSURE_POINTS[signal] || 6) * contextWeightForSignal(signal, known))
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
score: clamp(applied.reduce((sum, item) => sum + item.points, 0)),
|
|
136
|
+
applied
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function controlDimension(input = {}, known = null) {
|
|
141
|
+
const controls = controlsFrom(input, known);
|
|
142
|
+
const applied = controls.map((control) => ({
|
|
143
|
+
control,
|
|
144
|
+
points: CONTROL_POINTS[control] || 4
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
score: clamp(applied.reduce((sum, item) => sum + item.points, 0)),
|
|
148
|
+
applied,
|
|
149
|
+
verified_only: true
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function supplyChainDimension(input = {}, known = null) {
|
|
154
|
+
const records = sourceRecords(known);
|
|
155
|
+
const repository = records.find((item) => item.source_type === "github_repository_api")?.facts || {};
|
|
156
|
+
const release = records.find((item) => item.source_type === "github_release_api")?.facts || {};
|
|
157
|
+
const checks = [];
|
|
158
|
+
const sourceUrl = input.source_url || known?.source_url || "";
|
|
159
|
+
if (String(sourceUrl).startsWith("https://")) checks.push({ id: "canonical-https-source", points: 10 });
|
|
160
|
+
if (repository.license) checks.push({ id: "license-disclosed", points: 10 });
|
|
161
|
+
if (repository.pushed_at && Date.now() - Date.parse(repository.pushed_at) <= 90 * 86_400_000) {
|
|
162
|
+
checks.push({ id: "recent-maintenance", points: 15 });
|
|
163
|
+
}
|
|
164
|
+
if (release.latest_release) checks.push({ id: "published-release", points: 15 });
|
|
165
|
+
if (input.exact_version || input.commit_sha || input.package_digest) checks.push({ id: "install-artifact-pinned", points: 15 });
|
|
166
|
+
if (Number(repository.stars || known?.stars || 0) >= 100) checks.push({ id: "community-adoption-auxiliary", points: 5 });
|
|
167
|
+
if (known?.trust_signals?.includes("transparent-permissions")) checks.push({ id: "permissions-documented", points: 10 });
|
|
168
|
+
if (known?.trust_signals?.includes("signed-release-or-pinned-version")) checks.push({ id: "signed-or-pinned-release", points: 10 });
|
|
169
|
+
if (known?.intelligence_state === "strict_reviewed") checks.push({ id: "asl-source-review-complete", points: 10 });
|
|
170
|
+
if (repository.archived) checks.push({ id: "repository-archived", points: -20 });
|
|
171
|
+
return { score: clamp(checks.reduce((sum, item) => sum + item.points, 0)), applied: checks };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function evidenceDimension(known = null) {
|
|
175
|
+
if (!known) return { score: 0, checks: [], review_level: "L0_discovered" };
|
|
176
|
+
const groups = evidenceGroups(known);
|
|
177
|
+
const records = sourceRecords(known);
|
|
178
|
+
const scan = technicalScan(known);
|
|
179
|
+
const quality = qualityReview(known);
|
|
180
|
+
const checks = [];
|
|
181
|
+
if (known.source_url) checks.push({ id: "canonical-source", points: 15 });
|
|
182
|
+
if (records.some((item) => item.source_type === "github_release_api" && item.facts?.latest_release)) {
|
|
183
|
+
checks.push({ id: "version-or-release-evidence", points: 10 });
|
|
184
|
+
}
|
|
185
|
+
if (records.length >= 2) checks.push({ id: "multiple-structured-sources", points: 15 });
|
|
186
|
+
if (Number(scan?.files_scanned || 0) >= 1) checks.push({ id: "file-level-static-scan", points: 20 });
|
|
187
|
+
if (hasCommunityEvidence(records)) checks.push({ id: "community-source-check", points: 10 });
|
|
188
|
+
if (quality?.passed === true) checks.push({ id: "independent-recalculation", points: 15 });
|
|
189
|
+
if ((scan?.findings || []).some((item) => item.evidence?.sha)) checks.push({ id: "reproducible-source-reference", points: 5 });
|
|
190
|
+
if (known.runtime_validation?.passed === true) checks.push({ id: "runtime-sandbox-validation", points: 10 });
|
|
191
|
+
const score = clamp(checks.reduce((sum, item) => sum + item.points, 0));
|
|
192
|
+
const reviewLevel =
|
|
193
|
+
known.continuous_monitoring?.active === true && score >= 90
|
|
194
|
+
? "L4_continuously_monitored"
|
|
195
|
+
: known.runtime_validation?.passed === true && score >= 85
|
|
196
|
+
? "L3_runtime_validated"
|
|
197
|
+
: known.intelligence_state === "strict_reviewed" && score >= 80
|
|
198
|
+
? "L2_evidence_reviewed"
|
|
199
|
+
: score >= 30
|
|
200
|
+
? "L1_auto_assessed"
|
|
201
|
+
: "L0_discovered";
|
|
202
|
+
return { score, checks, review_level: reviewLevel };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function incidentDimension(input = {}, known = null) {
|
|
206
|
+
const incidents = [...(known?.incidents || []), ...(input.incidents || [])];
|
|
207
|
+
const scored = incidents.map((incident) => {
|
|
208
|
+
const reliability = Number(incident.evidence_reliability ?? incident.source_reliability ?? 0.5);
|
|
209
|
+
const severity = Number(incident.severity_score ?? 50);
|
|
210
|
+
const resolutionFactor = incident.resolution === "fixed" ? 0.25 : incident.resolution === "mitigated" ? 0.5 : 1;
|
|
211
|
+
const corroboration = incident.status === "confirmed" ? 1 : incident.status === "corroborated" ? 0.8 : 0.45;
|
|
212
|
+
return {
|
|
213
|
+
id: incident.id || incident.claim_type || "incident",
|
|
214
|
+
claim_type: incident.claim_type || "unknown",
|
|
215
|
+
score: clamp(severity * reliability * resolutionFactor * corroboration),
|
|
216
|
+
confirmed_critical:
|
|
217
|
+
incident.status === "confirmed" &&
|
|
218
|
+
incident.resolution !== "fixed" &&
|
|
219
|
+
CRITICAL_INCIDENT_TYPES.has(incident.claim_type)
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
score: scored.length ? Math.max(...scored.map((item) => item.score)) : 0,
|
|
224
|
+
incidents: scored,
|
|
225
|
+
auxiliary_sentiment_only: true
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hardFailures(risks = [], incident = {}) {
|
|
230
|
+
const failures = unique(risks.filter((signal) => HARD_FAILURE_SIGNALS.has(signal)));
|
|
231
|
+
if (incident.incidents?.some((item) => item.confirmed_critical)) failures.push("confirmed-critical-security-incident");
|
|
232
|
+
return unique(failures);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function requiredControls(risks = []) {
|
|
236
|
+
const controls = [];
|
|
237
|
+
if (risks.some((risk) => ["shell-execution", "subprocess-spawn", "remote-code-install", "dynamic-code-execution"].includes(risk))) {
|
|
238
|
+
controls.push("sandbox-or-container", "command-confirmation", "pinned-version");
|
|
239
|
+
}
|
|
240
|
+
if (risks.includes("filesystem-write")) controls.push("workspace-scope");
|
|
241
|
+
if (risks.includes("credential-access")) controls.push("scoped-credentials");
|
|
242
|
+
if (risks.includes("network-access")) controls.push("network-allowlist");
|
|
243
|
+
if (risks.includes("browser-access")) controls.push("isolated-browser-profile");
|
|
244
|
+
if (risks.some((risk) => ["background-execution", "scheduled-trigger", "workflow-automation"].includes(risk))) {
|
|
245
|
+
controls.push("audit-logging", "automatic-update-disabled");
|
|
246
|
+
}
|
|
247
|
+
return unique(controls);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function evaluateComponentSafety({ risks = [], known = null, input = {} }) {
|
|
251
|
+
const exposure = exposureDimension(risks, known);
|
|
252
|
+
const controls = controlDimension(input, known);
|
|
253
|
+
const supplyChain = supplyChainDimension(input, known);
|
|
254
|
+
const evidence = evidenceDimension(known);
|
|
255
|
+
const incident = incidentDimension(input, known);
|
|
256
|
+
const failures = hardFailures(risks, incident);
|
|
257
|
+
const required = requiredControls(risks);
|
|
258
|
+
const appliedControlIds = new Set(controls.applied.map((item) => item.control));
|
|
259
|
+
const missingControls = required.filter((control) => !appliedControlIds.has(control));
|
|
260
|
+
const residualRisk = clamp(exposure.score * (1 - controls.score / 125) + incident.score * 0.45);
|
|
261
|
+
const contextSafetyScore = clamp(
|
|
262
|
+
(100 - exposure.score) * 0.25 +
|
|
263
|
+
controls.score * 0.3 +
|
|
264
|
+
supplyChain.score * 0.25 +
|
|
265
|
+
evidence.score * 0.2 -
|
|
266
|
+
incident.score * 0.35
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
let decision = "allow_with_restrictions";
|
|
270
|
+
if (failures.length || incident.score >= 80) decision = "avoid";
|
|
271
|
+
else if (known?.intelligence_state !== "strict_reviewed" || evidence.score < 80) decision = "ask_user";
|
|
272
|
+
else if (residualRisk >= 55 || (exposure.score >= 60 && controls.score < 50)) decision = "ask_user";
|
|
273
|
+
else if (residualRisk <= 20 && controls.score >= 60 && supplyChain.score >= 65) decision = "allow";
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
standard: "ASL Agent Component Safety Standard",
|
|
277
|
+
model_version: "asl-safety-standard@0.2.0",
|
|
278
|
+
context_safety_score: contextSafetyScore,
|
|
279
|
+
dimensions: {
|
|
280
|
+
exposure_risk: exposure,
|
|
281
|
+
control_strength: controls,
|
|
282
|
+
supply_chain_trust: supplyChain,
|
|
283
|
+
evidence_confidence: evidence,
|
|
284
|
+
incident_risk: incident
|
|
285
|
+
},
|
|
286
|
+
residual_risk: residualRisk,
|
|
287
|
+
decision,
|
|
288
|
+
hard_failures: failures,
|
|
289
|
+
required_controls: required,
|
|
290
|
+
missing_controls: missingControls,
|
|
291
|
+
scoring_disclosure:
|
|
292
|
+
"The score evaluates this installation context. Powerful capabilities are exposure, not proof of malicious intent."
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
function isJsonLike(path) {
|
|
4
|
+
return path.endsWith(".json") || path.endsWith(".jsonc");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function makeObservation({ type, file, jsonPath, value, severityHint = "medium" }) {
|
|
8
|
+
return {
|
|
9
|
+
type,
|
|
10
|
+
path: file.relative_path,
|
|
11
|
+
json_path: jsonPath,
|
|
12
|
+
line: file.resolve_json_line ? file.resolve_json_line(jsonPath) : 1,
|
|
13
|
+
value,
|
|
14
|
+
severity_hint: severityHint
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pathSegments(jsonPath) {
|
|
19
|
+
if (jsonPath === "$") return [];
|
|
20
|
+
const normalized = jsonPath
|
|
21
|
+
.replace(/^\$\./, "")
|
|
22
|
+
.replace(/\[(\d+)\]/g, ".$1");
|
|
23
|
+
return normalized.split(".").filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildJsonLineIndex(content) {
|
|
27
|
+
const index = new Map();
|
|
28
|
+
const stack = [];
|
|
29
|
+
const lines = content.split(/\r?\n/);
|
|
30
|
+
|
|
31
|
+
index.set("$", 1);
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
const arrayItemMatch = line.match(/^\s*\{/);
|
|
36
|
+
if (arrayItemMatch && stack.length) {
|
|
37
|
+
const parentPath = ["$", ...stack.map((item) => item.key)].join(".");
|
|
38
|
+
if (!index.has(`${parentPath}.0`)) {
|
|
39
|
+
index.set(`${parentPath}.0`, i + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const keyMatch = line.match(/^\s*"([^"]+)"\s*:/);
|
|
44
|
+
if (!keyMatch) continue;
|
|
45
|
+
|
|
46
|
+
const indent = line.match(/^\s*/)[0].length;
|
|
47
|
+
while (stack.length && stack[stack.length - 1].indent >= indent) {
|
|
48
|
+
stack.pop();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = keyMatch[1];
|
|
52
|
+
const currentPath = ["$", ...stack.map((item) => item.key), key].join(".");
|
|
53
|
+
index.set(currentPath, i + 1);
|
|
54
|
+
|
|
55
|
+
const valuePart = line.slice(line.indexOf(":") + 1).trim();
|
|
56
|
+
if (valuePart.startsWith("{") || valuePart.startsWith("[")) {
|
|
57
|
+
stack.push({ key, indent });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findNearestLine(jsonLineIndex, jsonPath) {
|
|
66
|
+
const segments = pathSegments(jsonPath);
|
|
67
|
+
for (let end = segments.length; end >= 0; end -= 1) {
|
|
68
|
+
const candidate = end === 0 ? "$" : `$.${segments.slice(0, end).join(".")}`;
|
|
69
|
+
if (jsonLineIndex.has(candidate)) return jsonLineIndex.get(candidate);
|
|
70
|
+
}
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function walk(value, visitor, path = "$") {
|
|
75
|
+
visitor(value, path);
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
value.forEach((item, index) => walk(item, visitor, `${path}[${index}]`));
|
|
78
|
+
} else if (value && typeof value === "object") {
|
|
79
|
+
for (const [key, child] of Object.entries(value)) {
|
|
80
|
+
walk(child, visitor, `${path}.${key}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasAnyKey(value, keys) {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
87
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractMcpServerObservations(file, data, observations) {
|
|
91
|
+
walk(data, (value, path) => {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
93
|
+
|
|
94
|
+
const lowerPath = path.toLowerCase();
|
|
95
|
+
const isSpecificServer = lowerPath.includes(".servers.") || lowerPath.includes(".mcpservers.");
|
|
96
|
+
|
|
97
|
+
if (hasAnyKey(value, ["command", "args"]) && lowerPath.includes("mcp") && isSpecificServer) {
|
|
98
|
+
observations.push(makeObservation({
|
|
99
|
+
type: "mcp-stdio-process",
|
|
100
|
+
file,
|
|
101
|
+
jsonPath: path,
|
|
102
|
+
value,
|
|
103
|
+
severityHint: "high"
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const type = typeof value.type === "string" ? value.type.toLowerCase() : "";
|
|
108
|
+
const url = typeof value.url === "string" ? value.url : "";
|
|
109
|
+
if ((type.includes("sse") || type.includes("http") || /^https?:\/\//i.test(url)) && lowerPath.includes("mcp") && isSpecificServer) {
|
|
110
|
+
observations.push(makeObservation({
|
|
111
|
+
type: "mcp-remote-endpoint",
|
|
112
|
+
file,
|
|
113
|
+
jsonPath: path,
|
|
114
|
+
value,
|
|
115
|
+
severityHint: "medium"
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const serialized = JSON.stringify(value).toLowerCase();
|
|
120
|
+
if (lowerPath.includes("mcp") && isSpecificServer && serialized.includes("filesystem")) {
|
|
121
|
+
observations.push(makeObservation({
|
|
122
|
+
type: "mcp-filesystem-capability",
|
|
123
|
+
file,
|
|
124
|
+
jsonPath: path,
|
|
125
|
+
value,
|
|
126
|
+
severityHint: "high"
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractRemoteTriggerObservations(file, data, observations) {
|
|
133
|
+
walk(data, (value, path) => {
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
135
|
+
const lowerPath = path.toLowerCase();
|
|
136
|
+
const serialized = JSON.stringify(value).toLowerCase();
|
|
137
|
+
const hasChannelKey = ["telegram", "discord", "slack", "feishu", "wechat", "qq", "whatsapp", "signal", "gateway", "webhook"]
|
|
138
|
+
.some((key) => lowerPath.includes(key) || serialized.includes(key));
|
|
139
|
+
const hasPolicyKey = hasAnyKey(value, ["dmPolicy", "allowFrom", "groupPolicy", "allowed_users", "webhookSecret", "botToken"]);
|
|
140
|
+
|
|
141
|
+
if (hasChannelKey && hasPolicyKey) {
|
|
142
|
+
observations.push(makeObservation({
|
|
143
|
+
type: "remote-trigger-config",
|
|
144
|
+
file,
|
|
145
|
+
jsonPath: path,
|
|
146
|
+
value,
|
|
147
|
+
severityHint: "medium"
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractScheduledObservations(file, data, observations) {
|
|
154
|
+
walk(data, (value, path) => {
|
|
155
|
+
if (!value || typeof value !== "object") return;
|
|
156
|
+
const lowerPath = path.toLowerCase();
|
|
157
|
+
const isSpecificTask = path !== "$" && (/\[(\d+)\]$/.test(path) || hasAnyKey(value, ["cron", "schedule", "prompt", "task"]));
|
|
158
|
+
const hasDirectScheduleField = hasAnyKey(value, ["cron", "schedule"]);
|
|
159
|
+
const isSchedulePath = lowerPath.includes("cron") || lowerPath.includes("scheduled") || lowerPath.includes("schedule");
|
|
160
|
+
if (isSpecificTask && (hasDirectScheduleField || (/\[(\d+)\]$/.test(path) && isSchedulePath))) {
|
|
161
|
+
observations.push(makeObservation({
|
|
162
|
+
type: "scheduled-agent-execution",
|
|
163
|
+
file,
|
|
164
|
+
jsonPath: path,
|
|
165
|
+
value,
|
|
166
|
+
severityHint: "medium"
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function extractCredentialObservations(file, data, observations) {
|
|
173
|
+
walk(data, (value, path) => {
|
|
174
|
+
if (typeof value !== "string") return;
|
|
175
|
+
const text = `${path} ${value}`;
|
|
176
|
+
if (/(api_key|token|secret|private_key|botToken|webhookSecret|\$\{[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*\})/i.test(text)) {
|
|
177
|
+
observations.push(makeObservation({
|
|
178
|
+
type: "credential-reference",
|
|
179
|
+
file,
|
|
180
|
+
jsonPath: path,
|
|
181
|
+
value,
|
|
182
|
+
severityHint: "medium"
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function extractJsonObservations(files) {
|
|
189
|
+
const observations = [];
|
|
190
|
+
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (!isJsonLike(file.relative_path)) continue;
|
|
193
|
+
const content = await readFile(file.path, "utf8");
|
|
194
|
+
let data;
|
|
195
|
+
try {
|
|
196
|
+
data = JSON.parse(content);
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const lineIndex = buildJsonLineIndex(content);
|
|
202
|
+
file.resolve_json_line = (jsonPath) => findNearestLine(lineIndex, jsonPath);
|
|
203
|
+
|
|
204
|
+
extractMcpServerObservations(file, data, observations);
|
|
205
|
+
extractRemoteTriggerObservations(file, data, observations);
|
|
206
|
+
extractScheduledObservations(file, data, observations);
|
|
207
|
+
extractCredentialObservations(file, data, observations);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return observations;
|
|
211
|
+
}
|