@mcp-shark/mcp-shark 1.4.2 → 1.5.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 (203) hide show
  1. package/README.md +84 -645
  2. package/bin/mcp-shark.js +30 -36
  3. package/mcp-server/index.js +115 -0
  4. package/mcp-server/lib/auditor/audit.js +22 -38
  5. package/mcp-server/lib/common/error.js +1 -1
  6. package/mcp-server/lib/server/external/all.js +5 -6
  7. package/mcp-server/lib/server/external/config.js +1 -3
  8. package/mcp-server/lib/server/external/kv.js +4 -12
  9. package/mcp-server/lib/server/external/single/request.js +3 -6
  10. package/mcp-server/lib/server/external/single/run.js +8 -19
  11. package/mcp-server/lib/server/internal/handlers/prompts-get.js +3 -13
  12. package/mcp-server/lib/server/internal/handlers/prompts-list.js +2 -6
  13. package/mcp-server/lib/server/internal/handlers/resources-list.js +2 -6
  14. package/mcp-server/lib/server/internal/handlers/resources-read.js +3 -12
  15. package/mcp-server/lib/server/internal/handlers/tools-call.js +3 -9
  16. package/mcp-server/lib/server/internal/handlers/tools-list.js +2 -2
  17. package/mcp-server/lib/server/internal/run.js +4 -16
  18. package/mcp-server/lib/server/internal/server.js +6 -7
  19. package/mcp-server/lib/server/internal/session.js +2 -15
  20. package/mcp-server/mcp-shark.js +16 -66
  21. package/package.json +23 -38
  22. package/ui/dist/assets/index-Cc-IUa83.css +1 -0
  23. package/ui/dist/assets/index-srLDlk97.js +35 -0
  24. package/ui/dist/index.html +17 -0
  25. package/ui/dist/og-image.png +0 -0
  26. package/ui/server/routes/backups/deleteBackup.js +54 -0
  27. package/ui/server/routes/backups/index.js +15 -0
  28. package/ui/server/routes/backups/listBackups.js +75 -0
  29. package/ui/server/routes/backups/restoreBackup.js +83 -0
  30. package/ui/server/routes/backups/viewBackup.js +47 -0
  31. package/ui/server/routes/composite/index.js +46 -0
  32. package/ui/server/routes/composite/servers.js +18 -0
  33. package/ui/server/routes/composite/setup.js +129 -0
  34. package/ui/server/routes/composite/status.js +7 -0
  35. package/ui/server/routes/composite/stop.js +39 -0
  36. package/ui/server/routes/composite/utils.js +45 -0
  37. package/ui/server/routes/config.js +34 -30
  38. package/ui/server/routes/conversations.js +3 -3
  39. package/ui/server/routes/help.js +2 -2
  40. package/ui/server/routes/logs.js +5 -5
  41. package/ui/server/routes/playground.js +45 -47
  42. package/ui/server/routes/requests.js +112 -108
  43. package/ui/server/routes/sessions.js +4 -4
  44. package/ui/server/routes/settings.js +199 -0
  45. package/ui/server/routes/smartscan/discover.js +7 -6
  46. package/ui/server/routes/smartscan/scans/clearCache.js +3 -2
  47. package/ui/server/routes/smartscan/scans/createBatchScans.js +4 -3
  48. package/ui/server/routes/smartscan/scans/createScan.js +2 -1
  49. package/ui/server/routes/smartscan/scans/getCachedResults.js +2 -1
  50. package/ui/server/routes/smartscan/scans/getScan.js +2 -1
  51. package/ui/server/routes/smartscan/scans/listScans.js +5 -4
  52. package/ui/server/routes/smartscan/scans.js +3 -3
  53. package/ui/server/routes/smartscan/token.js +4 -3
  54. package/ui/server/routes/smartscan/transport.js +1 -1
  55. package/ui/server/routes/smartscan.js +1 -1
  56. package/ui/server/routes/statistics.js +13 -10
  57. package/ui/server/utils/config-update.js +7 -6
  58. package/ui/server/utils/config.js +4 -4
  59. package/ui/server/utils/logger.js +2 -0
  60. package/ui/server/utils/paths.js +210 -2
  61. package/ui/server/utils/port.js +2 -2
  62. package/ui/server/utils/process.js +0 -67
  63. package/ui/server/utils/scan-cache/all-results.js +76 -59
  64. package/ui/server/utils/scan-cache/directory.js +1 -1
  65. package/ui/server/utils/scan-cache/file-operations.js +19 -16
  66. package/ui/server/utils/scan-cache/server-operations.js +14 -9
  67. package/ui/server/utils/serialization.js +9 -3
  68. package/ui/server/utils/smartscan-token.js +4 -3
  69. package/ui/server.js +86 -41
  70. package/ui/src/App.jsx +5 -5
  71. package/ui/src/CompositeLogs.jsx +20 -20
  72. package/ui/src/CompositeSetup.jsx +9 -9
  73. package/ui/src/HelpGuide/HelpGuideFooter.jsx +1 -0
  74. package/ui/src/HelpGuide/HelpGuideHeader.jsx +2 -1
  75. package/ui/src/HelpGuide.jsx +17 -4
  76. package/ui/src/IntroTour.jsx +19 -5
  77. package/ui/src/LogDetail.jsx +1 -0
  78. package/ui/src/LogTable.jsx +24 -6
  79. package/ui/src/PacketDetail.jsx +21 -16
  80. package/ui/src/PacketFilters.jsx +29 -14
  81. package/ui/src/PacketList.jsx +4 -5
  82. package/ui/src/SmartScan.jsx +5 -5
  83. package/ui/src/TabNavigation.jsx +5 -5
  84. package/ui/src/components/App/HelpButton.jsx +4 -0
  85. package/ui/src/components/App/TrafficTab.jsx +4 -4
  86. package/ui/src/components/App/useAppState.js +118 -24
  87. package/ui/src/components/BackupList.jsx +6 -2
  88. package/ui/src/components/CollapsibleSection.jsx +16 -2
  89. package/ui/src/components/ConfigViewerModal.jsx +17 -3
  90. package/ui/src/components/ConfirmationModal.jsx +20 -3
  91. package/ui/src/components/DetailsTab/BodySection.jsx +3 -1
  92. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +14 -3
  93. package/ui/src/components/DetailsTab/InfoSection.jsx +4 -2
  94. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +7 -5
  95. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +7 -5
  96. package/ui/src/components/DetectedPathsList.jsx +5 -2
  97. package/ui/src/components/FileInput.jsx +3 -1
  98. package/ui/src/components/GroupHeader.jsx +14 -0
  99. package/ui/src/components/GroupedByMcpView.jsx +3 -4
  100. package/ui/src/components/GroupedByServerView.jsx +1 -1
  101. package/ui/src/components/GroupedBySessionView.jsx +1 -1
  102. package/ui/src/components/HexTab.jsx +17 -4
  103. package/ui/src/components/LogsToolbar.jsx +3 -1
  104. package/ui/src/components/McpPlayground/LoadingModal.jsx +7 -3
  105. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +5 -0
  106. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +6 -2
  107. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +4 -4
  108. package/ui/src/components/McpPlayground/PromptsSection.jsx +2 -1
  109. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +3 -0
  110. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +6 -2
  111. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +4 -4
  112. package/ui/src/components/McpPlayground/ResourcesSection.jsx +2 -1
  113. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +5 -0
  114. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +6 -2
  115. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +4 -4
  116. package/ui/src/components/McpPlayground/ToolsSection.jsx +2 -1
  117. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +9 -9
  118. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +10 -5
  119. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +43 -23
  120. package/ui/src/components/McpPlayground/useMcpPlayground.js +72 -44
  121. package/ui/src/components/McpPlayground.jsx +5 -2
  122. package/ui/src/components/PacketDetailHeader.jsx +8 -3
  123. package/ui/src/components/PacketFilters/ExportControls.jsx +2 -1
  124. package/ui/src/components/PacketFilters/FilterInput.jsx +1 -1
  125. package/ui/src/components/RawTab.jsx +15 -2
  126. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +10 -2
  127. package/ui/src/components/RequestRow/RequestRowMain.jsx +12 -3
  128. package/ui/src/components/RequestRow/ResponseRow.jsx +11 -3
  129. package/ui/src/components/RequestRow.jsx +17 -9
  130. package/ui/src/components/ServerControl.jsx +3 -1
  131. package/ui/src/components/ServiceSelector.jsx +2 -0
  132. package/ui/src/components/SmartScan/AnalysisResult.jsx +2 -2
  133. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +2 -1
  134. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +1 -1
  135. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +9 -3
  136. package/ui/src/components/SmartScan/EmptyState.jsx +3 -0
  137. package/ui/src/components/SmartScan/ErrorDisplay.jsx +4 -2
  138. package/ui/src/components/SmartScan/ExpandableSection.jsx +4 -0
  139. package/ui/src/components/SmartScan/FindingsTable.jsx +46 -42
  140. package/ui/src/components/SmartScan/ListViewContent.jsx +2 -2
  141. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +13 -8
  142. package/ui/src/components/SmartScan/OverallSummarySection.jsx +36 -29
  143. package/ui/src/components/SmartScan/RecommendationsSection.jsx +10 -8
  144. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +2 -1
  145. package/ui/src/components/SmartScan/ScanDetailView.jsx +4 -4
  146. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +2 -1
  147. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +15 -1
  148. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +3 -1
  149. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +1 -1
  150. package/ui/src/components/SmartScan/ScanViewContent.jsx +2 -2
  151. package/ui/src/components/SmartScan/ScanningProgress.jsx +4 -2
  152. package/ui/src/components/SmartScan/ServerInfoSection.jsx +3 -1
  153. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +4 -2
  154. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +5 -3
  155. package/ui/src/components/SmartScan/SmartScanControls.jsx +11 -7
  156. package/ui/src/components/SmartScan/SmartScanHeader.jsx +1 -1
  157. package/ui/src/components/SmartScan/ViewModeTabs.jsx +2 -0
  158. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +1 -2
  159. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +22 -26
  160. package/ui/src/components/SmartScan/hooks/useScanList.js +10 -9
  161. package/ui/src/components/SmartScan/hooks/useScanOperations.js +23 -14
  162. package/ui/src/components/SmartScan/hooks/useServerStatus.js +2 -2
  163. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +2 -2
  164. package/ui/src/components/SmartScan/scanDataUtils.js +22 -17
  165. package/ui/src/components/SmartScan/useSmartScan.js +4 -4
  166. package/ui/src/components/SmartScan/utils.js +3 -1
  167. package/ui/src/components/SmartScanIcons.jsx +6 -3
  168. package/ui/src/components/TabNavigation/DesktopTabs.jsx +8 -3
  169. package/ui/src/components/TabNavigation/MobileDropdown.jsx +3 -1
  170. package/ui/src/components/TabNavigation.jsx +8 -3
  171. package/ui/src/components/TabNavigationIcons.jsx +4 -4
  172. package/ui/src/components/TourOverlay.jsx +1 -1
  173. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +3 -0
  174. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +1 -0
  175. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +9 -0
  176. package/ui/src/components/TourTooltip/useTooltipPosition.js +63 -36
  177. package/ui/src/components/TourTooltip.jsx +11 -3
  178. package/ui/src/components/ViewModeTabs.jsx +3 -1
  179. package/ui/src/config/tourSteps.jsx +0 -2
  180. package/ui/src/hooks/useAnimation.js +15 -12
  181. package/ui/src/hooks/useConfigManagement.js +8 -8
  182. package/ui/src/hooks/useServiceExtraction.js +1 -1
  183. package/ui/src/index.css +3 -8
  184. package/ui/src/theme.js +3 -3
  185. package/ui/src/utils/hexUtils.js +11 -5
  186. package/ui/src/utils/mcpGroupingUtils.js +18 -10
  187. package/ui/src/utils/requestPairing.js +89 -0
  188. package/ui/src/utils/requestUtils.js +32 -101
  189. package/ui/vite.config.js +1 -1
  190. package/mcp-server/.editorconfig +0 -15
  191. package/mcp-server/.prettierignore +0 -11
  192. package/mcp-server/.prettierrc +0 -12
  193. package/mcp-server/README.md +0 -280
  194. package/mcp-server/commitlint.config.cjs +0 -42
  195. package/mcp-server/eslint.config.js +0 -131
  196. package/mcp-server/package-lock.json +0 -4784
  197. package/mcp-server/package.json +0 -30
  198. package/ui/README.md +0 -212
  199. package/ui/package-lock.json +0 -3574
  200. package/ui/package.json +0 -12
  201. package/ui/paths.js +0 -282
  202. package/ui/server/routes/backups.js +0 -251
  203. package/ui/server/routes/composite.js +0 -260
@@ -1,11 +1,11 @@
1
+ import { IconChevronDown } from '@tabler/icons-react';
1
2
  import { colors, fonts, withOpacity } from '../../theme';
2
3
  import {
3
- formatRelativeTime,
4
4
  formatDateTime,
5
- getSourceDest,
5
+ formatRelativeTime,
6
6
  getEndpoint,
7
+ getSourceDest,
7
8
  } from '../../utils/requestUtils.js';
8
- import { IconChevronDown } from '@tabler/icons-react';
9
9
 
10
10
  const ChevronDown = ({ size = 12, rotated = false }) => (
11
11
  <IconChevronDown
@@ -39,6 +39,14 @@ export default function RequestRowMain({
39
39
  <>
40
40
  <tr
41
41
  onClick={() => onSelect(request)}
42
+ onKeyDown={(e) => {
43
+ if (e.key === 'Enter' || e.key === ' ') {
44
+ e.preventDefault();
45
+ onSelect(request);
46
+ }
47
+ }}
48
+ tabIndex={0}
49
+ aria-label={`Select request ${request.frame_number}`}
42
50
  style={{
43
51
  cursor: 'pointer',
44
52
  background: isSelected
@@ -84,6 +92,7 @@ export default function RequestRowMain({
84
92
  >
85
93
  {hasResponse && (
86
94
  <button
95
+ type="button"
87
96
  onClick={(e) => {
88
97
  e.stopPropagation();
89
98
  onToggleExpand();
@@ -1,12 +1,12 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import {
3
- formatRelativeTime,
4
3
  formatDateTime,
5
- getSourceDest,
4
+ formatRelativeTime,
6
5
  getEndpoint,
6
+ getSourceDest,
7
7
  } from '../../utils/requestUtils.js';
8
8
 
9
- export default function ResponseRow({ response, selected, firstRequestTime, onSelect, request }) {
9
+ export default function ResponseRow({ response, selected, firstRequestTime, onSelect }) {
10
10
  const isSelected = selected?.frame_number === response.frame_number;
11
11
  const { source, dest } = getSourceDest(response);
12
12
  const relativeTime = formatRelativeTime(response.timestamp_iso, firstRequestTime);
@@ -14,6 +14,14 @@ export default function ResponseRow({ response, selected, firstRequestTime, onSe
14
14
  return (
15
15
  <tr
16
16
  onClick={() => onSelect(response)}
17
+ onKeyDown={(e) => {
18
+ if (e.key === 'Enter' || e.key === ' ') {
19
+ e.preventDefault();
20
+ onSelect(response);
21
+ }
22
+ }}
23
+ tabIndex={0}
24
+ aria-label={`Select response ${response.frame_number}`}
17
25
  style={{
18
26
  cursor: 'pointer',
19
27
  background:
@@ -12,17 +12,23 @@ function RequestRow({
12
12
  onToggleExpand = () => {},
13
13
  }) {
14
14
  // Support both pair prop (new) and request prop (legacy for grouped views)
15
- let request, response;
16
- if (pair) {
17
- request = pair.request;
18
- response = pair.response;
19
- } else if (requestProp) {
20
- request = requestProp;
21
- response = null;
22
- } else {
15
+ const extractRequestResponse = (pair, requestProp) => {
16
+ if (pair) {
17
+ return { request: pair.request, response: pair.response };
18
+ }
19
+ if (requestProp) {
20
+ return { request: requestProp, response: null };
21
+ }
22
+ return null;
23
+ };
24
+
25
+ const data = extractRequestResponse(pair, requestProp);
26
+ if (!data) {
23
27
  return null; // No valid data
24
28
  }
25
29
 
30
+ const { request, response } = data;
31
+
26
32
  // Check if this is an unpaired request or response
27
33
  const isUnpaired = !request || !response;
28
34
 
@@ -38,7 +44,9 @@ function RequestRow({
38
44
  );
39
45
  }
40
46
 
41
- if (!request) return null; // Only show rows that have a request
47
+ if (!request) {
48
+ return null; // Only show rows that have a request
49
+ }
42
50
 
43
51
  const hasResponse = !!response;
44
52
 
@@ -29,6 +29,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
29
29
  <div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
30
30
  {status.running ? (
31
31
  <button
32
+ type="button"
32
33
  onClick={onStop}
33
34
  disabled={loading}
34
35
  style={{
@@ -60,6 +61,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
60
61
  </button>
61
62
  ) : (
62
63
  <button
64
+ type="button"
63
65
  data-tour="start-button"
64
66
  onClick={onStart}
65
67
  disabled={loading || !canStart}
@@ -122,7 +124,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
122
124
  fontFamily: fonts.body,
123
125
  }}
124
126
  >
125
- {status.running ? `Running (PID: ${status.pid})` : 'Stopped'}
127
+ {status.running ? (status.pid ? `Running (PID: ${status.pid})` : 'Running') : 'Stopped'}
126
128
  </span>
127
129
  </div>
128
130
  </div>
@@ -40,6 +40,7 @@ function ServiceSelector({ services, selectedServices, onSelectionChange }) {
40
40
  }}
41
41
  >
42
42
  <button
43
+ type="button"
43
44
  onClick={() => {
44
45
  onSelectionChange(new Set(services.map((s) => s.name)));
45
46
  }}
@@ -56,6 +57,7 @@ function ServiceSelector({ services, selectedServices, onSelectionChange }) {
56
57
  Select All
57
58
  </button>
58
59
  <button
60
+ type="button"
59
61
  onClick={() => {
60
62
  onSelectionChange(new Set());
61
63
  }}
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
  import FindingsTable from './FindingsTable';
4
+ import NotablePatternsSection from './NotablePatternsSection';
4
5
  import OverallSummarySection from './OverallSummarySection';
5
6
  import RecommendationsSection from './RecommendationsSection';
6
- import NotablePatternsSection from './NotablePatternsSection';
7
7
 
8
8
  export default function AnalysisResult({ analysis }) {
9
9
  if (!analysis) {
@@ -11,7 +11,7 @@ export default function AnalysisResult({ analysis }) {
11
11
  <div
12
12
  style={{
13
13
  padding: '12px',
14
- background: colors.bgTertiary + '80',
14
+ background: `${colors.bgTertiary}80`,
15
15
  borderRadius: '6px',
16
16
  border: `1px solid ${colors.borderLight}`,
17
17
  fontSize: '12px',
@@ -1,6 +1,6 @@
1
- import { CheckIcon, CacheIcon, ExternalLinkIcon } from '../../SmartScanIcons';
2
1
  import { IconEye } from '@tabler/icons-react';
3
2
  import { colors, fonts } from '../../../theme';
3
+ import { CacheIcon, CheckIcon, ExternalLinkIcon } from '../../SmartScanIcons';
4
4
  import { getRiskLevelColor } from '../utils';
5
5
 
6
6
  export default function BatchResultItem({ result, onViewScan }) {
@@ -64,6 +64,7 @@ export default function BatchResultItem({ result, onViewScan }) {
64
64
  )}
65
65
  {onViewScan && result.data && (
66
66
  <button
67
+ type="button"
67
68
  onClick={() => onViewScan(result.data)}
68
69
  style={{
69
70
  display: 'inline-flex',
@@ -1,5 +1,5 @@
1
- import { CheckIcon, CacheIcon } from '../../SmartScanIcons';
2
1
  import { colors, fonts } from '../../../theme';
2
+ import { CacheIcon, CheckIcon } from '../../SmartScanIcons';
3
3
 
4
4
  export default function BatchResultsHeader({ scanResults }) {
5
5
  const cachedCount = scanResults.filter((r) => r.cached).length;
@@ -1,9 +1,11 @@
1
1
  import { colors } from '../../theme';
2
- import BatchResultsHeader from './BatchResultsDisplay/BatchResultsHeader';
3
2
  import BatchResultItem from './BatchResultsDisplay/BatchResultItem';
3
+ import BatchResultsHeader from './BatchResultsDisplay/BatchResultsHeader';
4
4
 
5
5
  export default function BatchResultsDisplay({ scanResults, onViewScan }) {
6
- if (scanResults.length === 0) return null;
6
+ if (scanResults.length === 0) {
7
+ return null;
8
+ }
7
9
 
8
10
  return (
9
11
  <div
@@ -18,7 +20,11 @@ export default function BatchResultsDisplay({ scanResults, onViewScan }) {
18
20
  <BatchResultsHeader scanResults={scanResults} />
19
21
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
20
22
  {scanResults.map((result, idx) => (
21
- <BatchResultItem key={idx} result={result} onViewScan={onViewScan} />
23
+ <BatchResultItem
24
+ key={result.scanId || `batch-result-${idx}`}
25
+ result={result}
26
+ onViewScan={onViewScan}
27
+ />
22
28
  ))}
23
29
  </div>
24
30
  </div>
@@ -24,7 +24,10 @@ export default function EmptyState() {
24
24
  strokeLinecap="round"
25
25
  strokeLinejoin="round"
26
26
  style={{ opacity: 0.5 }}
27
+ role="img"
28
+ aria-label="Empty state icon"
27
29
  >
30
+ <title>Empty state icon</title>
28
31
  <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
29
32
  <path d="M9 12l2 2 4-4" />
30
33
  </svg>
@@ -1,8 +1,10 @@
1
- import { AlertIcon } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { AlertIcon } from '../SmartScanIcons';
3
3
 
4
4
  export default function ErrorDisplay({ error }) {
5
- if (!error) return null;
5
+ if (!error) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <div
@@ -14,6 +14,7 @@ export default function ExpandableSection({ title, count, children, defaultExpan
14
14
  }}
15
15
  >
16
16
  <button
17
+ type="button"
17
18
  onClick={() => setIsExpanded(!isExpanded)}
18
19
  style={{
19
20
  width: '100%',
@@ -73,7 +74,10 @@ export default function ExpandableSection({ title, count, children, defaultExpan
73
74
  fill="none"
74
75
  stroke="currentColor"
75
76
  viewBox="0 0 24 24"
77
+ role="img"
78
+ aria-label="Chevron icon"
76
79
  >
80
+ <title>Chevron icon</title>
77
81
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
78
82
  </svg>
79
83
  </button>
@@ -110,7 +110,7 @@ export default function FindingsTable({ findings, type }) {
110
110
  <tbody>
111
111
  {findings.map((finding, index) => (
112
112
  <tr
113
- key={index}
113
+ key={`finding-${finding.id || finding.name || index}-${index}`}
114
114
  style={{
115
115
  borderBottom: `1px solid ${colors.borderLight}`,
116
116
  transition: 'background 0.15s',
@@ -164,7 +164,7 @@ export default function FindingsTable({ findings, type }) {
164
164
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
165
165
  {finding.risk_tags?.map((tag, tagIndex) => (
166
166
  <span
167
- key={tagIndex}
167
+ key={`tag-${String(tag)}-${tagIndex}`}
168
168
  style={{
169
169
  padding: '2px 6px',
170
170
  background: colors.bgCard,
@@ -192,7 +192,10 @@ export default function FindingsTable({ findings, type }) {
192
192
  style={{ listStyle: 'disc', listStylePosition: 'inside', margin: 0, padding: 0 }}
193
193
  >
194
194
  {finding.reasons?.map((reason, reasonIndex) => (
195
- <li key={reasonIndex} style={{ fontSize: '10px', marginBottom: '2px' }}>
195
+ <li
196
+ key={`reason-${reasonIndex}-${reason.substring(0, 20)}`}
197
+ style={{ fontSize: '10px', marginBottom: '2px' }}
198
+ >
196
199
  {reason}
197
200
  </li>
198
201
  ))}
@@ -209,45 +212,46 @@ export default function FindingsTable({ findings, type }) {
209
212
  >
210
213
  {finding.safe_use_notes}
211
214
  </td>
212
- {type === 'tool' && finding.hasOwnProperty('is_potentially_poisoned') && (
213
- <td style={{ padding: '8px' }}>
214
- {finding.is_potentially_poisoned ? (
215
- <span
216
- style={{
217
- display: 'inline-flex',
218
- alignItems: 'center',
219
- padding: '2px 6px',
220
- fontSize: '10px',
221
- fontWeight: '500',
222
- borderRadius: '4px',
223
- background: colors.error + '20',
224
- color: colors.error,
225
- border: `1px solid ${colors.error}40`,
226
- fontFamily: fonts.body,
227
- }}
228
- >
229
- Yes
230
- </span>
231
- ) : (
232
- <span
233
- style={{
234
- display: 'inline-flex',
235
- alignItems: 'center',
236
- padding: '2px 6px',
237
- fontSize: '10px',
238
- fontWeight: '500',
239
- borderRadius: '4px',
240
- background: colors.accentGreen + '20',
241
- color: colors.accentGreen,
242
- border: `1px solid ${colors.accentGreen}40`,
243
- fontFamily: fonts.body,
244
- }}
245
- >
246
- No
247
- </span>
248
- )}
249
- </td>
250
- )}
215
+ {type === 'tool' &&
216
+ Object.prototype.hasOwnProperty.call(finding, 'is_potentially_poisoned') && (
217
+ <td style={{ padding: '8px' }}>
218
+ {finding.is_potentially_poisoned ? (
219
+ <span
220
+ style={{
221
+ display: 'inline-flex',
222
+ alignItems: 'center',
223
+ padding: '2px 6px',
224
+ fontSize: '10px',
225
+ fontWeight: '500',
226
+ borderRadius: '4px',
227
+ background: `${colors.error}20`,
228
+ color: colors.error,
229
+ border: `1px solid ${colors.error}40`,
230
+ fontFamily: fonts.body,
231
+ }}
232
+ >
233
+ Yes
234
+ </span>
235
+ ) : (
236
+ <span
237
+ style={{
238
+ display: 'inline-flex',
239
+ alignItems: 'center',
240
+ padding: '2px 6px',
241
+ fontSize: '10px',
242
+ fontWeight: '500',
243
+ borderRadius: '4px',
244
+ background: `${colors.accentGreen}20`,
245
+ color: colors.accentGreen,
246
+ border: `1px solid ${colors.accentGreen}40`,
247
+ fontFamily: fonts.body,
248
+ }}
249
+ >
250
+ No
251
+ </span>
252
+ )}
253
+ </td>
254
+ )}
251
255
  </tr>
252
256
  ))}
253
257
  </tbody>
@@ -1,6 +1,6 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import ScanResultsDisplay from './ScanResultsDisplay';
3
2
  import ScanDetailView from './ScanDetailView';
3
+ import ScanResultsDisplay from './ScanResultsDisplay';
4
4
 
5
5
  export default function ListViewContent({
6
6
  error,
@@ -50,7 +50,7 @@ export default function ListViewContent({
50
50
  <div
51
51
  style={{
52
52
  padding: '12px 16px',
53
- background: colors.error + '20',
53
+ background: `${colors.error}20`,
54
54
  border: `1px solid ${colors.error}`,
55
55
  borderRadius: '8px',
56
56
  marginBottom: '16px',
@@ -2,31 +2,33 @@ import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
 
4
4
  export default function NotablePatternsSection({ patterns }) {
5
- if (!patterns || patterns.length === 0) return null;
5
+ if (!patterns || patterns.length === 0) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <ExpandableSection title="Notable Patterns" count={patterns.length} defaultExpanded={false}>
9
11
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
10
12
  {patterns.map((pattern, index) => (
11
13
  <div
12
- key={index}
14
+ key={`pattern-${pattern.type || index}-${index}`}
13
15
  style={{
14
16
  display: 'flex',
15
17
  alignItems: 'flex-start',
16
18
  gap: '8px',
17
19
  padding: '8px',
18
- background: colors.accentOrange + '10',
20
+ background: `${colors.accentOrange}10`,
19
21
  borderRadius: '6px',
20
22
  border: `1px solid ${colors.accentOrange}20`,
21
23
  transition: 'all 0.15s',
22
24
  }}
23
25
  onMouseEnter={(e) => {
24
- e.currentTarget.style.background = colors.accentOrange + '15';
25
- e.currentTarget.style.borderColor = colors.accentOrange + '40';
26
+ e.currentTarget.style.background = `${colors.accentOrange}15`;
27
+ e.currentTarget.style.borderColor = `${colors.accentOrange}40`;
26
28
  }}
27
29
  onMouseLeave={(e) => {
28
- e.currentTarget.style.background = colors.accentOrange + '10';
29
- e.currentTarget.style.borderColor = colors.accentOrange + '20';
30
+ e.currentTarget.style.background = `${colors.accentOrange}10`;
31
+ e.currentTarget.style.borderColor = `${colors.accentOrange}20`;
30
32
  }}
31
33
  >
32
34
  <div
@@ -36,7 +38,7 @@ export default function NotablePatternsSection({ patterns }) {
36
38
  width: '16px',
37
39
  height: '16px',
38
40
  borderRadius: '50%',
39
- background: colors.accentOrange + '30',
41
+ background: `${colors.accentOrange}30`,
40
42
  border: `1px solid ${colors.accentOrange}60`,
41
43
  display: 'flex',
42
44
  alignItems: 'center',
@@ -47,7 +49,10 @@ export default function NotablePatternsSection({ patterns }) {
47
49
  style={{ width: '10px', height: '10px', color: colors.accentOrange }}
48
50
  fill="currentColor"
49
51
  viewBox="0 0 20 20"
52
+ role="img"
53
+ aria-label="Pattern icon"
50
54
  >
55
+ <title>Pattern icon</title>
51
56
  <path
52
57
  fillRule="evenodd"
53
58
  d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
@@ -1,9 +1,42 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import { getRiskLevelColor } from './utils';
3
2
  import ExpandableSection from './ExpandableSection';
3
+ import { getRiskLevelColor } from './utils';
4
+
5
+ function renderReasonContent(overallReason) {
6
+ const separator = overallReason.includes('\n')
7
+ ? '\n'
8
+ : overallReason.includes(' | ')
9
+ ? ' | '
10
+ : null;
11
+
12
+ if (separator) {
13
+ return (
14
+ <ul
15
+ style={{
16
+ listStyle: 'disc',
17
+ listStylePosition: 'inside',
18
+ margin: 0,
19
+ paddingLeft: '8px',
20
+ }}
21
+ >
22
+ {overallReason.split(separator).map((item, index) => (
23
+ <li
24
+ key={`reason-${index}-${item.trim().substring(0, 20)}`}
25
+ style={{ fontSize: '12px', marginBottom: '2px' }}
26
+ >
27
+ {item.trim()}
28
+ </li>
29
+ ))}
30
+ </ul>
31
+ );
32
+ }
33
+ return <p style={{ fontSize: '12px' }}>{overallReason}</p>;
34
+ }
4
35
 
5
36
  export default function OverallSummarySection({ overallRiskLevel, overallReason }) {
6
- if (!overallRiskLevel) return null;
37
+ if (!overallRiskLevel) {
38
+ return null;
39
+ }
7
40
 
8
41
  return (
9
42
  <ExpandableSection title="Overall Summary" count={overallReason ? 1 : 0} defaultExpanded={true}>
@@ -37,33 +70,7 @@ export default function OverallSummarySection({ overallRiskLevel, overallReason
37
70
  </div>
38
71
  {overallReason && (
39
72
  <div style={{ fontSize: '12px', color: colors.textSecondary, fontFamily: fonts.body }}>
40
- {(() => {
41
- const separator = overallReason.includes('\n')
42
- ? '\n'
43
- : overallReason.includes(' | ')
44
- ? ' | '
45
- : null;
46
-
47
- if (separator) {
48
- return (
49
- <ul
50
- style={{
51
- listStyle: 'disc',
52
- listStylePosition: 'inside',
53
- margin: 0,
54
- paddingLeft: '8px',
55
- }}
56
- >
57
- {overallReason.split(separator).map((item, index) => (
58
- <li key={index} style={{ fontSize: '12px', marginBottom: '2px' }}>
59
- {item.trim()}
60
- </li>
61
- ))}
62
- </ul>
63
- );
64
- }
65
- return <p style={{ fontSize: '12px' }}>{overallReason}</p>;
66
- })()}
73
+ {renderReasonContent(overallReason)}
67
74
  </div>
68
75
  )}
69
76
  </div>
@@ -2,7 +2,9 @@ import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
 
4
4
  export default function RecommendationsSection({ recommendations }) {
5
- if (!recommendations || recommendations.length === 0) return null;
5
+ if (!recommendations || recommendations.length === 0) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <ExpandableSection
@@ -13,24 +15,24 @@ export default function RecommendationsSection({ recommendations }) {
13
15
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
14
16
  {recommendations.map((recommendation, index) => (
15
17
  <div
16
- key={index}
18
+ key={`recommendation-${recommendation.type || index}-${index}`}
17
19
  style={{
18
20
  display: 'flex',
19
21
  alignItems: 'flex-start',
20
22
  gap: '8px',
21
23
  padding: '8px',
22
- background: colors.accentBlue + '10',
24
+ background: `${colors.accentBlue}10`,
23
25
  borderRadius: '6px',
24
26
  border: `1px solid ${colors.accentBlue}20`,
25
27
  transition: 'all 0.15s',
26
28
  }}
27
29
  onMouseEnter={(e) => {
28
- e.currentTarget.style.background = colors.accentBlue + '15';
29
- e.currentTarget.style.borderColor = colors.accentBlue + '40';
30
+ e.currentTarget.style.background = `${colors.accentBlue}15`;
31
+ e.currentTarget.style.borderColor = `${colors.accentBlue}40`;
30
32
  }}
31
33
  onMouseLeave={(e) => {
32
- e.currentTarget.style.background = colors.accentBlue + '10';
33
- e.currentTarget.style.borderColor = colors.accentBlue + '20';
34
+ e.currentTarget.style.background = `${colors.accentBlue}10`;
35
+ e.currentTarget.style.borderColor = `${colors.accentBlue}20`;
34
36
  }}
35
37
  >
36
38
  <div
@@ -40,7 +42,7 @@ export default function RecommendationsSection({ recommendations }) {
40
42
  width: '16px',
41
43
  height: '16px',
42
44
  borderRadius: '50%',
43
- background: colors.accentBlue + '30',
45
+ background: `${colors.accentBlue}30`,
44
46
  border: `1px solid ${colors.accentBlue}60`,
45
47
  display: 'flex',
46
48
  alignItems: 'center',
@@ -1,5 +1,5 @@
1
+ import { IconExternalLink, IconX } from '@tabler/icons-react';
1
2
  import { colors, fonts } from '../../theme';
2
- import { IconX, IconExternalLink } from '@tabler/icons-react';
3
3
 
4
4
  export default function ScanDetailHeader({ scanId, serverName, onClose }) {
5
5
  return (
@@ -63,6 +63,7 @@ export default function ScanDetailHeader({ scanId, serverName, onClose }) {
63
63
  </a>
64
64
  )}
65
65
  <button
66
+ type="button"
66
67
  onClick={onClose}
67
68
  style={{
68
69
  padding: '6px',
@@ -1,11 +1,11 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import AnalysisResult from './AnalysisResult';
3
- import { normalizeScanData } from './scanDataUtils';
4
- import ScanDetailHeader from './ScanDetailHeader';
5
3
  import DebugInfoSection from './DebugInfoSection';
4
+ import RawDataSection from './RawDataSection';
5
+ import ScanDetailHeader from './ScanDetailHeader';
6
6
  import ScanOverviewSection from './ScanOverviewSection';
7
7
  import ServerInfoSection from './ServerInfoSection';
8
- import RawDataSection from './RawDataSection';
8
+ import { normalizeScanData } from './scanDataUtils';
9
9
 
10
10
  export default function ScanDetailView({ scan, loading, onClose }) {
11
11
  if (loading) {
@@ -120,7 +120,7 @@ export default function ScanDetailView({ scan, loading, onClose }) {
120
120
  <div
121
121
  style={{
122
122
  padding: '12px',
123
- background: colors.bgTertiary + '80',
123
+ background: `${colors.bgTertiary}80`,
124
124
  borderRadius: '6px',
125
125
  border: `1px solid ${colors.borderLight}`,
126
126
  fontSize: '12px',
@@ -1,5 +1,5 @@
1
- import { colors, fonts } from '../../../theme';
2
1
  import { IconRefresh } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../../../theme';
3
3
 
4
4
  export default function ScanListHeader({ scanCount, loading, onRefresh }) {
5
5
  return (
@@ -23,6 +23,7 @@ export default function ScanListHeader({ scanCount, loading, onRefresh }) {
23
23
  All Scans ({scanCount})
24
24
  </h2>
25
25
  <button
26
+ type="button"
26
27
  onClick={onRefresh}
27
28
  disabled={loading}
28
29
  style={{