@mcp-shark/mcp-shark 1.5.13 → 1.7.2

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 (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Cross-server toxic-flow analysis from observed MCP traffic (tools/list responses).
3
+ * Complements CLI static scan: same analyzeToxicFlows heuristics, fed by proxy captures.
4
+ */
5
+ import { analyzeToxicFlows } from '#core/cli/ToxicFlowAnalyzer.js';
6
+ import {
7
+ toolsFromJsonrpcResultString,
8
+ toolsFromTrafficResponseBody,
9
+ } from './toolsListFromTrafficParser.js';
10
+
11
+ const TRAFFIC_IDE = 'Traffic';
12
+ const DEBOUNCE_MS = 400;
13
+
14
+ function serverKey(mcpServerName, sessionId) {
15
+ if (mcpServerName && String(mcpServerName).trim()) {
16
+ return String(mcpServerName).trim();
17
+ }
18
+ if (sessionId && String(sessionId).trim()) {
19
+ return `session:${String(sessionId).trim()}`;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export class TrafficToxicFlowService {
25
+ constructor(packetRepository, logger) {
26
+ this.packetRepository = packetRepository;
27
+ this.logger = logger;
28
+ /** @type {Map<string, { tools: object[], updatedAt: number }>} */
29
+ this._byServer = new Map();
30
+ this._debounceTimer = null;
31
+ this._lastFlows = [];
32
+ this._lastComputedAt = null;
33
+ this._lastReplayPacketCount = 0;
34
+ }
35
+
36
+ /**
37
+ * Ingest a proxied JSON-RPC response (live path).
38
+ * @param {{ mcpServerName?: string|null, sessionId?: string|null, body?: unknown }} packetData
39
+ */
40
+ ingestFromTrafficResponse(packetData) {
41
+ const tools = toolsFromTrafficResponseBody(packetData?.body);
42
+ if (!tools?.length) {
43
+ return;
44
+ }
45
+ const key = serverKey(packetData?.mcpServerName, packetData?.sessionId);
46
+ if (!key) {
47
+ this.logger?.debug?.('Traffic toxic flow: skip tools/list — no server name or session');
48
+ return;
49
+ }
50
+ this._byServer.set(key, { tools, updatedAt: Date.now() });
51
+ this._scheduleRecompute();
52
+ }
53
+
54
+ _scheduleRecompute() {
55
+ if (this._debounceTimer) {
56
+ clearTimeout(this._debounceTimer);
57
+ }
58
+ this._debounceTimer = setTimeout(() => {
59
+ this._debounceTimer = null;
60
+ this._recomputeNow();
61
+ }, DEBOUNCE_MS);
62
+ }
63
+
64
+ _serversForAnalyzer() {
65
+ const servers = [];
66
+ for (const [name, entry] of this._byServer.entries()) {
67
+ servers.push({
68
+ name,
69
+ ide: TRAFFIC_IDE,
70
+ config: {},
71
+ tools: entry.tools,
72
+ });
73
+ }
74
+ return servers;
75
+ }
76
+
77
+ _recomputeNow() {
78
+ const servers = this._serversForAnalyzer();
79
+ this._lastFlows = servers.length >= 2 ? analyzeToxicFlows(servers) : [];
80
+ this._lastComputedAt = Date.now();
81
+ this.logger?.debug?.(
82
+ { serverCount: servers.length, flowCount: this._lastFlows.length },
83
+ 'Traffic toxic flows recomputed'
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Batch replay: rebuild registry from sqlite packets, then recompute.
89
+ * @returns {{ packetRows: number, serverCount: number, flowCount: number }}
90
+ */
91
+ rebuildFromDatabase() {
92
+ if (!this.packetRepository?.listResponsesWithToolsList) {
93
+ this.logger?.warn?.('Traffic toxic flow replay: packet repository unavailable');
94
+ return { packetRows: 0, serverCount: 0, flowCount: 0 };
95
+ }
96
+ const rows = this.packetRepository.listResponsesWithToolsList();
97
+ this._byServer.clear();
98
+ this._lastReplayPacketCount = rows.length;
99
+
100
+ for (const row of rows) {
101
+ let tools = toolsFromJsonrpcResultString(row.jsonrpc_result);
102
+ if (!tools?.length) {
103
+ tools = toolsFromTrafficResponseBody(row.body_json || row.body_raw);
104
+ }
105
+ if (!tools?.length) {
106
+ continue;
107
+ }
108
+ const key = serverKey(row.remote_address, row.session_id);
109
+ if (!key) {
110
+ continue;
111
+ }
112
+ this._byServer.set(key, { tools, updatedAt: Date.now() });
113
+ }
114
+
115
+ this._recomputeNow();
116
+ return {
117
+ packetRows: rows.length,
118
+ serverCount: this._byServer.size,
119
+ flowCount: this._lastFlows.length,
120
+ };
121
+ }
122
+
123
+ getSnapshot() {
124
+ const servers = [];
125
+ for (const [name, entry] of this._byServer.entries()) {
126
+ servers.push({
127
+ name,
128
+ toolCount: entry.tools.length,
129
+ updatedAt: entry.updatedAt,
130
+ });
131
+ }
132
+ return {
133
+ success: true,
134
+ toxicFlows: this._lastFlows,
135
+ servers,
136
+ computedAt: this._lastComputedAt,
137
+ lastReplayPacketCount: this._lastReplayPacketCount,
138
+ note:
139
+ 'Heuristic cross-server pairs from tools seen in tools/list traffic (HTTP proxy). ' +
140
+ 'Not runtime taint tracking. Requires at least two distinct server keys and overlapping capability rules.',
141
+ };
142
+ }
143
+
144
+ clear() {
145
+ this._byServer.clear();
146
+ this._lastFlows = [];
147
+ this._lastComputedAt = null;
148
+ this._lastReplayPacketCount = 0;
149
+ if (this._debounceTimer) {
150
+ clearTimeout(this._debounceTimer);
151
+ this._debounceTimer = null;
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * AAuth knowledge-graph builder.
3
+ *
4
+ * Walks the captured packets, extracts AAuth signals via aauthParser, and
5
+ * shapes a node/edge graph that mirrors the categories on
6
+ * https://mcp-shark.github.io/aauth-explorer/ — but every node here is
7
+ * grounded in observed traffic, not a static spec illustration.
8
+ *
9
+ * Categories (kept stable so the UI can color-code them):
10
+ * - agent — observed AAuth-Agent values
11
+ * - mission — observed AAuth-Mission values
12
+ * - resource — destination host/server names
13
+ * - signing — algorithms parsed from Signature-Input (`alg=...`)
14
+ * - access — access modes parsed from AAuth-Requirement (`mode=...`)
15
+ *
16
+ * Edges are weighted by observed packet count so the force layout settles
17
+ * naturally around the busiest nodes.
18
+ */
19
+
20
+ import { parseAauthForPacket } from './aauthParser.js';
21
+
22
+ const ACCESS_MODE_REGEX = /mode\s*=\s*"?([A-Za-z0-9_-]+)"?/i;
23
+
24
+ function nodeKey(category, id) {
25
+ return `${category}::${id}`;
26
+ }
27
+
28
+ function ensureNode(map, category, id, extra = {}) {
29
+ const key = nodeKey(category, id);
30
+ if (!map.has(key)) {
31
+ map.set(key, {
32
+ id: key,
33
+ name: id,
34
+ category,
35
+ packet_count: 0,
36
+ sample_frame_numbers: [],
37
+ ...extra,
38
+ });
39
+ }
40
+ return map.get(key);
41
+ }
42
+
43
+ function bumpNode(node, frameNumber) {
44
+ node.packet_count += 1;
45
+ if (frameNumber != null && node.sample_frame_numbers.length < 5) {
46
+ node.sample_frame_numbers.push(frameNumber);
47
+ }
48
+ }
49
+
50
+ function ensureEdge(map, sourceKey, targetKey, kind) {
51
+ const key = `${sourceKey}->${targetKey}::${kind}`;
52
+ if (!map.has(key)) {
53
+ map.set(key, { id: key, source: sourceKey, target: targetKey, kind, weight: 0 });
54
+ }
55
+ return map.get(key);
56
+ }
57
+
58
+ function bumpEdge(edge) {
59
+ edge.weight += 1;
60
+ }
61
+
62
+ function extractAccessMode(requirement) {
63
+ if (!requirement || typeof requirement !== 'string') {
64
+ return null;
65
+ }
66
+ const m = requirement.match(ACCESS_MODE_REGEX);
67
+ return m ? m[1] : null;
68
+ }
69
+
70
+ function extractResource(packet) {
71
+ const host = packet?.host || packet?.remote_address || null;
72
+ if (host) {
73
+ return host;
74
+ }
75
+ const url = packet?.url;
76
+ if (typeof url === 'string') {
77
+ try {
78
+ return new URL(url).host;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Build an AAuth knowledge graph from a list of packets.
88
+ *
89
+ * @param {Array} packets - packets as returned by RequestService.getRequests
90
+ * @returns {{
91
+ * categories: Array<{id:string,label:string}>,
92
+ * nodes: Array,
93
+ * edges: Array,
94
+ * stats: object,
95
+ * }}
96
+ */
97
+ export function buildAauthGraph(packets) {
98
+ const nodes = new Map();
99
+ const edges = new Map();
100
+
101
+ let observedSignals = 0;
102
+
103
+ for (const packet of packets || []) {
104
+ const aauth = parseAauthForPacket(packet);
105
+ const hasAnySignal =
106
+ aauth.posture !== 'none' ||
107
+ aauth.agent ||
108
+ aauth.mission ||
109
+ aauth.requirement ||
110
+ aauth.sig_alg;
111
+ if (!hasAnySignal) {
112
+ continue;
113
+ }
114
+ observedSignals += 1;
115
+
116
+ const resourceId = extractResource(packet);
117
+ const frame = packet?.frame_number ?? null;
118
+
119
+ const resourceNode = resourceId
120
+ ? ensureNode(nodes, 'resource', resourceId, { url: packet?.url || null })
121
+ : null;
122
+ if (resourceNode) {
123
+ bumpNode(resourceNode, frame);
124
+ }
125
+
126
+ let agentNode = null;
127
+ if (aauth.agent) {
128
+ agentNode = ensureNode(nodes, 'agent', aauth.agent, {
129
+ last_keyid: aauth.sig_keyid,
130
+ posture: aauth.posture,
131
+ });
132
+ bumpNode(agentNode, frame);
133
+ }
134
+
135
+ let missionNode = null;
136
+ if (aauth.mission) {
137
+ missionNode = ensureNode(nodes, 'mission', aauth.mission);
138
+ bumpNode(missionNode, frame);
139
+ }
140
+
141
+ let signingNode = null;
142
+ if (aauth.sig_alg) {
143
+ signingNode = ensureNode(nodes, 'signing', aauth.sig_alg, {
144
+ keyid: aauth.sig_keyid,
145
+ });
146
+ bumpNode(signingNode, frame);
147
+ }
148
+
149
+ const accessMode = extractAccessMode(aauth.requirement);
150
+ let accessNode = null;
151
+ if (accessMode) {
152
+ accessNode = ensureNode(nodes, 'access', accessMode);
153
+ bumpNode(accessNode, frame);
154
+ }
155
+
156
+ if (agentNode && resourceNode) {
157
+ bumpEdge(ensureEdge(edges, agentNode.id, resourceNode.id, 'calls'));
158
+ }
159
+ if (agentNode && missionNode) {
160
+ bumpEdge(ensureEdge(edges, agentNode.id, missionNode.id, 'pursues'));
161
+ }
162
+ if (missionNode && resourceNode) {
163
+ bumpEdge(ensureEdge(edges, missionNode.id, resourceNode.id, 'targets'));
164
+ }
165
+ if (agentNode && signingNode) {
166
+ bumpEdge(ensureEdge(edges, agentNode.id, signingNode.id, 'signs-with'));
167
+ }
168
+ if (resourceNode && accessNode) {
169
+ bumpEdge(ensureEdge(edges, resourceNode.id, accessNode.id, 'requires'));
170
+ }
171
+ }
172
+
173
+ const categories = [
174
+ { id: 'agent', label: 'Agent' },
175
+ { id: 'mission', label: 'Mission' },
176
+ { id: 'resource', label: 'Resource' },
177
+ { id: 'signing', label: 'Signing' },
178
+ { id: 'access', label: 'Access' },
179
+ ];
180
+
181
+ return {
182
+ categories,
183
+ nodes: [...nodes.values()],
184
+ edges: [...edges.values()],
185
+ stats: {
186
+ observed_packets: observedSignals,
187
+ node_counts: countByCategory([...nodes.values()]),
188
+ edge_count: edges.size,
189
+ },
190
+ };
191
+ }
192
+
193
+ function countByCategory(nodes) {
194
+ const out = {};
195
+ for (const n of nodes) {
196
+ out[n.category] = (out[n.category] || 0) + 1;
197
+ }
198
+ return out;
199
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * AAuth Visibility Parser
3
+ *
4
+ * Pure, observer-only parser for AAuth artifacts as they appear in HTTP
5
+ * headers and (incidentally) MCP server config. Does NOT perform any
6
+ * cryptographic verification — every signal it returns is described as
7
+ * "observed" or "present", never "valid".
8
+ *
9
+ * Specs informally referenced:
10
+ * - HTTP Message Signatures (RFC 9421) — Signature, Signature-Input
11
+ * - draft-hardt-http-signature-keys (AAuth) — Signature-Key, Signature-Error
12
+ * - draft-hardt-aauth-protocol — AAuth-Agent, AAuth-Mission,
13
+ * AAuth-Requirement
14
+ *
15
+ * The parser is intentionally liberal in what it accepts: AAuth drafts evolve,
16
+ * and visibility-only is the right place to record what is on the wire.
17
+ */
18
+
19
+ const AAUTH_AGENT_ID_REGEX = /aauth:[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
20
+
21
+ const SIGNATURE_HEADER_NAMES = ['signature', 'signature-input'];
22
+ const AAUTH_HEADER_PREFIX = 'aauth-';
23
+
24
+ /**
25
+ * Lower-case all header keys, returning a new object.
26
+ * Tolerates non-object input by returning {}.
27
+ */
28
+ function normalizeHeaders(headers) {
29
+ if (!headers || typeof headers !== 'object') {
30
+ return {};
31
+ }
32
+ const out = {};
33
+ for (const key of Object.keys(headers)) {
34
+ out[key.toLowerCase()] = headers[key];
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * Parse a Signature-Input value into its first label's covered components and
41
+ * algorithm parameter. Format example:
42
+ * sig1=("@method" "@target-uri" "host");keyid="abc";alg="ed25519";created=1700000000
43
+ *
44
+ * Returns { label, covered, alg, keyid, created } or null on failure.
45
+ */
46
+ function parseSignatureInput(value) {
47
+ if (!value || typeof value !== 'string') {
48
+ return null;
49
+ }
50
+ const labelMatch = value.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*\(([^)]*)\)(.*)$/);
51
+ if (!labelMatch) {
52
+ return null;
53
+ }
54
+ const [, label, componentsRaw, paramsRaw] = labelMatch;
55
+ const covered = [];
56
+ const componentRegex = /"([^"]+)"/g;
57
+ let m = componentRegex.exec(componentsRaw);
58
+ while (m) {
59
+ covered.push(m[1]);
60
+ m = componentRegex.exec(componentsRaw);
61
+ }
62
+
63
+ const params = {};
64
+ const paramRegex = /;\s*([A-Za-z0-9_-]+)=("([^"]*)"|([0-9]+))/g;
65
+ let pm = paramRegex.exec(paramsRaw);
66
+ while (pm) {
67
+ const [, key, , quoted, raw] = pm;
68
+ params[key] = quoted !== undefined ? quoted : raw;
69
+ pm = paramRegex.exec(paramsRaw);
70
+ }
71
+
72
+ return {
73
+ label,
74
+ covered,
75
+ alg: params.alg || null,
76
+ keyid: params.keyid || null,
77
+ created: params.created ? Number(params.created) : null,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Truncate a key/thumbprint string to a UI-friendly preview while keeping
83
+ * enough characters to be visually distinguishable.
84
+ */
85
+ function shortFingerprint(value) {
86
+ if (!value || typeof value !== 'string') {
87
+ return null;
88
+ }
89
+ const trimmed = value.replace(/^"|"$/g, '').trim();
90
+ if (trimmed.length <= 16) {
91
+ return trimmed;
92
+ }
93
+ return `${trimmed.slice(0, 8)}…${trimmed.slice(-4)}`;
94
+ }
95
+
96
+ /**
97
+ * Extract any AAuth agent identifier(s) from a string. Returns the first match
98
+ * or null. Matches the form `aauth:<local>@<domain>` from the AAuth draft.
99
+ */
100
+ export function extractAgentId(text) {
101
+ if (!text || typeof text !== 'string') {
102
+ return null;
103
+ }
104
+ AAUTH_AGENT_ID_REGEX.lastIndex = 0;
105
+ const m = text.match(AAUTH_AGENT_ID_REGEX);
106
+ return m ? m[0] : null;
107
+ }
108
+
109
+ /**
110
+ * Parse AAuth signals from a single packet's headers.
111
+ *
112
+ * Returns a normalized envelope. Every field is purely observational; the
113
+ * caller MUST NOT treat any of these values as cryptographically validated.
114
+ *
115
+ * @param {object|string|null|undefined} headers
116
+ * @returns {{
117
+ * posture: 'signed' | 'aauth-aware' | 'bearer' | 'none',
118
+ * sig_present: boolean,
119
+ * sig_alg: string | null,
120
+ * sig_keyid: string | null,
121
+ * sig_keyid_short: string | null,
122
+ * sig_covered: string[],
123
+ * key_thumbprint: string | null,
124
+ * key_thumbprint_short: string | null,
125
+ * agent: string | null,
126
+ * mission: string | null,
127
+ * requirement: string | null,
128
+ * error: string | null,
129
+ * raw: Record<string, string>
130
+ * }}
131
+ */
132
+ export function parseAauthHeaders(headers) {
133
+ const h = normalizeHeaders(headers);
134
+
135
+ const sigInputRaw = h['signature-input'] || null;
136
+ const sigRaw = h.signature || null;
137
+ const sigKeyRaw = h['signature-key'] || null;
138
+ const sigErrorRaw = h['signature-error'] || null;
139
+ const agentRaw = h['aauth-agent'] || null;
140
+ const missionRaw = h['aauth-mission'] || null;
141
+ const requirementRaw = h['aauth-requirement'] || h['www-authenticate-aauth'] || null;
142
+ const authzRaw = h.authorization || null;
143
+
144
+ const parsedInput = parseSignatureInput(sigInputRaw);
145
+
146
+ const sigPresent = Boolean(sigRaw && sigInputRaw);
147
+ const sigAlg = parsedInput?.alg || null;
148
+ const sigKeyid = parsedInput?.keyid || null;
149
+ const sigCovered = parsedInput?.covered || [];
150
+
151
+ const agent = agentRaw || extractAgentId(authzRaw) || extractAgentId(sigKeyRaw) || null;
152
+
153
+ const aauthHeaderNames = Object.keys(h).filter((k) => k.startsWith(AAUTH_HEADER_PREFIX));
154
+ const aauthAware = aauthHeaderNames.length > 0 || Boolean(sigKeyRaw);
155
+
156
+ let posture;
157
+ if (sigPresent) {
158
+ posture = 'signed';
159
+ } else if (aauthAware) {
160
+ posture = 'aauth-aware';
161
+ } else if (authzRaw && /^bearer\s/i.test(authzRaw)) {
162
+ posture = 'bearer';
163
+ } else {
164
+ posture = 'none';
165
+ }
166
+
167
+ const raw = {};
168
+ for (const name of [
169
+ ...SIGNATURE_HEADER_NAMES,
170
+ 'signature-key',
171
+ 'signature-error',
172
+ 'aauth-agent',
173
+ 'aauth-mission',
174
+ 'aauth-requirement',
175
+ ]) {
176
+ if (h[name]) {
177
+ raw[name] = h[name];
178
+ }
179
+ }
180
+
181
+ return {
182
+ posture,
183
+ sig_present: sigPresent,
184
+ sig_alg: sigAlg,
185
+ sig_keyid: sigKeyid,
186
+ sig_keyid_short: shortFingerprint(sigKeyid),
187
+ sig_covered: sigCovered,
188
+ key_thumbprint: sigKeyRaw,
189
+ key_thumbprint_short: shortFingerprint(sigKeyRaw),
190
+ agent,
191
+ mission: missionRaw,
192
+ requirement: requirementRaw,
193
+ error: sigErrorRaw,
194
+ raw,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Parse AAuth signals from a packet record (shape returned by PacketRepository).
200
+ * Reads `headers_json` (string or object).
201
+ */
202
+ export function parseAauthForPacket(packet) {
203
+ if (!packet) {
204
+ return parseAauthHeaders(null);
205
+ }
206
+ let headers = packet.headers_json || packet.headers;
207
+ if (typeof headers === 'string') {
208
+ try {
209
+ headers = JSON.parse(headers);
210
+ } catch {
211
+ headers = {};
212
+ }
213
+ }
214
+ return parseAauthHeaders(headers);
215
+ }
216
+
217
+ /**
218
+ * Summarize AAuth posture across a list of packets. Useful for the inventory
219
+ * panel and CLI summary line.
220
+ *
221
+ * Returns counts by posture and the set of unique agents/missions observed.
222
+ */
223
+ export function summarizeAauth(packets) {
224
+ const counts = { signed: 0, 'aauth-aware': 0, bearer: 0, none: 0 };
225
+ const agents = new Set();
226
+ const missions = new Set();
227
+ let total = 0;
228
+
229
+ for (const packet of packets || []) {
230
+ const aauth = parseAauthForPacket(packet);
231
+ counts[aauth.posture] = (counts[aauth.posture] || 0) + 1;
232
+ total += 1;
233
+ if (aauth.agent) {
234
+ agents.add(aauth.agent);
235
+ }
236
+ if (aauth.mission) {
237
+ missions.add(aauth.mission);
238
+ }
239
+ }
240
+
241
+ return {
242
+ total,
243
+ counts,
244
+ unique_agents: [...agents],
245
+ unique_missions: [...missions],
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Decide whether a config-level server entry advertises AAuth in any way we
251
+ * can recognize without a network call. This is intentionally narrow: presence
252
+ * of an `aauth_id`, a JWKS URL, or a `.well-known/aauth` URL.
253
+ *
254
+ * Returns null if no signal, otherwise an object describing what we saw.
255
+ */
256
+ export function inspectServerConfigForAauth(serverConfig) {
257
+ if (!serverConfig || typeof serverConfig !== 'object') {
258
+ return null;
259
+ }
260
+ const blob = JSON.stringify(serverConfig);
261
+ const aauthId = extractAgentId(blob);
262
+ const wellKnown = blob.match(/https?:\/\/[^"'\s]+\/\.well-known\/aauth[^"'\s]*/);
263
+ const jwksUrl = blob.match(/https?:\/\/[^"'\s]+\/jwks(?:\.json)?[^"'\s]*/i);
264
+
265
+ if (!aauthId && !wellKnown && !jwksUrl) {
266
+ return null;
267
+ }
268
+
269
+ return {
270
+ agent: aauthId || null,
271
+ well_known_url: wellKnown ? wellKnown[0] : null,
272
+ jwks_url: jwksUrl ? jwksUrl[0] : null,
273
+ };
274
+ }