@lievclouds/dsh-cortex 0.3.6 → 0.4.1

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.

Potentially problematic release.


This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.

package/index.js CHANGED
@@ -46,9 +46,12 @@ export function apply(ctx, config) {
46
46
  'When the user asks for ANY authorized penetration-testing work (渗透测试 / SRC / 漏洞挖掘 / 安全评估 / 资产与接口侦察 / 内网横向 / 漏洞复测), use the Cortex tools proactively instead of relying on memory:',
47
47
  '- Reuse memory first: `cortex_list_snapshots` + `cortex_get_snapshot` for task progress, `cortex_list_vulns` + `cortex_get_asset` for existing findings/assets; `cortex_list_assets` before repeating recon.',
48
48
  '- Persist confirmed findings: dedupe with `cortex_list_vulns`, then `cortex_save_vuln` (reproduction_steps in Markdown).',
49
- '- Persist recon context: `cortex_sync_asset_info` (mode=merge) for fingerprints, interfaces (raw HTTP), leaks and test parameters.',
49
+ '- Persist recon context: `cortex_sync_asset_info` (mode=merge) for fingerprints, interfaces (canonical fields path/method/params/request), leaked_infos (info_type/value/source/potential_impact) and test_parameters (name/endpoint/location/values/source); aliases are normalized but the canonical shape is preferred.',
50
50
  '- Status / retest: `cortex_update_vuln_status`, `cortex_append_retest` (append-only, never overwrite original evidence).',
51
- '- Summaries: `cortex_internal_overview`, `cortex_graph_summary`.',
51
+ '- Task progress (认知快照): at task milestones, before context compaction and at session end call `cortex_save_snapshot` (project, phase, targets, unfinished tasks, failed paths, tested interfaces, notes, next steps); pass session_id to update an existing snapshot.',
52
+ '- Project names: reuse the existing project string exactly (list the assets or the audit first). Variants such as `上海春芝堂 SRC` and `上海春芝堂_SRC` are merged defensively, but do not invent a new spelling.',
53
+ '- Data completeness is mandatory: before finishing a phase call `cortex_audit_completeness`, then fill every gap — every asset must carry interfaces (path/method/params/request), leaked_infos (info_type/value/source/potential_impact) and test_parameters (name/endpoint/location/values/source); every confirmed finding must be saved with Markdown reproduction_steps; internal hosts/services/footholds/credentials/pivots go through `cortex_sync_internal_scope`.',
54
+ '- Summaries / audit: `cortex_internal_overview`, `cortex_graph_summary`, `cortex_audit_completeness`.',
52
55
  'Do not skip these calls merely because no skill was loaded. If a tool call fails, report the failure; never claim a record was saved without a successful result.',
53
56
  'Examples: “列出 Cortex 里的高危漏洞”, “把刚才确认的漏洞写入 Cortex”, “把 127.0.0.1:8080 的接口同步到资产库”, “把 VULN-… 标记为已确认并追加复测结论”.',
54
57
  ].join('\n'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lievclouds/dsh-cortex",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "Cortex workbench for DeepSeek Harness: the original Cortex UI (vulnerabilities, assets, internal operations, Memory Nebula, snapshots, retest) as a native DSH panel, plus 12 native agent tools so DSH conversations use the local Cortex SQLite without MCP.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,6 +1,7 @@
1
1
  // Bundle entry for the ported Cortex UI. Built by esbuild into an IIFE global
2
2
  // (`__DSH_CORTEX_UI__`); the DSH wrapper client.js mounts it inside a Shadow DOM
3
3
  // so the original Tailwind stylesheet never touches the DSH shell.
4
+ import { Component, type ReactNode } from 'react'
4
5
  import { createRoot } from 'react-dom/client'
5
6
  import App from '../renderer/App'
6
7
  import { setPortalContainer } from './portal'
@@ -9,6 +10,80 @@ export interface CortexUiHandle {
9
10
  destroy(): void
10
11
  }
11
12
 
13
+ interface BoundaryState {
14
+ error: Error | null
15
+ }
16
+
17
+ /**
18
+ * Last line of defence: a render crash in any page used to unmount the whole
19
+ * React tree and leave a blank white panel. Show the error and a reload button
20
+ * instead so the failure is visible and recoverable.
21
+ */
22
+ class CortexErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {
23
+ state: BoundaryState = { error: null }
24
+
25
+ static getDerivedStateFromError(error: Error): BoundaryState {
26
+ return { error }
27
+ }
28
+
29
+ componentDidCatch(error: Error): void {
30
+ console.error('[dsh-cortex] Cortex renderer crashed', error)
31
+ }
32
+
33
+ render(): ReactNode {
34
+ if (this.state.error === null) return this.props.children
35
+ return (
36
+ <div
37
+ style={{
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ alignItems: 'center',
41
+ justifyContent: 'center',
42
+ gap: 12,
43
+ height: '100%',
44
+ padding: 24,
45
+ fontFamily: 'Inter, "Microsoft YaHei UI", sans-serif',
46
+ fontSize: 13,
47
+ color: '#334155',
48
+ background: '#f8fafc',
49
+ }}
50
+ >
51
+ <strong style={{ fontSize: 15 }}>Cortex 面板渲染失败</strong>
52
+ <pre
53
+ style={{
54
+ maxWidth: '90%',
55
+ maxHeight: '40vh',
56
+ overflow: 'auto',
57
+ whiteSpace: 'pre-wrap',
58
+ fontSize: 11.5,
59
+ padding: 10,
60
+ border: '1px solid #e2e8f0',
61
+ borderRadius: 8,
62
+ background: '#ffffff',
63
+ color: '#b91c1c',
64
+ }}
65
+ >
66
+ {String(this.state.error.message || this.state.error)}
67
+ </pre>
68
+ <button
69
+ type="button"
70
+ onClick={() => window.location.reload()}
71
+ style={{
72
+ padding: '6px 14px',
73
+ border: '1px solid #cbd5e1',
74
+ borderRadius: 6,
75
+ background: '#ffffff',
76
+ fontSize: 12,
77
+ cursor: 'pointer',
78
+ }}
79
+ >
80
+ 重新加载页面
81
+ </button>
82
+ </div>
83
+ )
84
+ }
85
+ }
86
+
12
87
  /**
13
88
  * Mount the original Cortex renderer into `host`.
14
89
  * `css` is the Tailwind build with fonts inlined as data URLs.
@@ -32,7 +107,11 @@ export function mount(host: HTMLElement, css: string): CortexUiHandle {
32
107
  document.documentElement.lang = document.documentElement.lang || 'zh-CN'
33
108
 
34
109
  const root = createRoot(rootElement)
35
- root.render(<App />)
110
+ root.render(
111
+ <CortexErrorBoundary>
112
+ <App />
113
+ </CortexErrorBoundary>,
114
+ )
36
115
 
37
116
  return {
38
117
  destroy() {
@@ -33,6 +33,8 @@ import { AssetInternalEntry, scopeForEntry } from '@renderer/components/internal
33
33
  import {
34
34
  INTERFACE_METHOD_STYLES,
35
35
  buildInterfaceRequest,
36
+ normalizeDisplayInterfaces,
37
+ normalizeDisplayTestParameters,
36
38
  techStackTags
37
39
  } from '@renderer/lib/assets'
38
40
  import { apiUnavailableMessage, getApi, toErrorMessage } from '@renderer/lib/api'
@@ -353,9 +355,9 @@ export function AssetDetail({
353
355
  }, [assetProjectName, dataVersion, retryKey, internalRetryKey, t])
354
356
 
355
357
  const interfaces = asset
356
- ? (asset.interfaces ?? normalizeAssetInterfaces([], asset.interface_dir))
358
+ ? normalizeDisplayInterfaces(asset.interfaces ?? normalizeAssetInterfaces([], asset.interface_dir))
357
359
  : []
358
- const testParameters = asset?.test_parameters ?? []
360
+ const testParameters = asset ? normalizeDisplayTestParameters(asset.test_parameters) : []
359
361
  const tags = asset ? techStackTags(asset.tech_stack) : []
360
362
  const internalScope =
361
363
  asset === null
@@ -2,6 +2,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
2
2
  import { Loader2, Trash2 } from 'lucide-react'
3
3
  import { Button } from '@renderer/components/ui/button'
4
4
  import { useI18n } from '@renderer/i18n'
5
+ import { getPortalContainer } from '../../../dsh/portal'
5
6
 
6
7
  interface DeleteAssetDialogProps {
7
8
  open: boolean
@@ -27,8 +28,8 @@ export function DeleteAssetDialog({
27
28
  const { t } = useI18n()
28
29
 
29
30
  return (
30
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
31
- <DialogPrimitive.Portal>
31
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
32
+ <DialogPrimitive.Portal container={getPortalContainer()}>
32
33
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
33
34
  <DialogPrimitive.Content className="fixed left-1/2 top-1/2 z-[70] w-[min(420px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
34
35
  <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
@@ -67,7 +67,9 @@ function TestParameterCard({
67
67
  }) {
68
68
  const { t, locationLabel } = useI18n()
69
69
  const { copied, copy } = useCopy()
70
- const valuesText = testParameterValuesText(parameter.values)
70
+ const values = parameter.values ?? []
71
+ const relatedVulnIds = parameter.related_vuln_ids ?? []
72
+ const valuesText = testParameterValuesText(values)
71
73
 
72
74
  return (
73
75
  <article
@@ -120,11 +122,11 @@ function TestParameterCard({
120
122
  <div className="min-w-0">
121
123
  <p className="text-[11px] font-medium text-foreground/70">
122
124
  {t('asset.parameter.values')}
123
- {parameter.values.length > 0 ? ` · ${parameter.values.length}` : ''}
125
+ {values.length > 0 ? ` · ${values.length}` : ''}
124
126
  </p>
125
- {parameter.values.length > 0 ? (
127
+ {values.length > 0 ? (
126
128
  <div className="mt-1 flex flex-wrap gap-1">
127
- {parameter.values.map((value, index) => (
129
+ {values.map((value, index) => (
128
130
  <span
129
131
  key={`${value}-${index}`}
130
132
  data-testid="asset-test-parameter-value"
@@ -153,13 +155,13 @@ function TestParameterCard({
153
155
  />
154
156
  ) : null}
155
157
 
156
- {parameter.related_vuln_ids.length > 0 ? (
158
+ {relatedVulnIds.length > 0 ? (
157
159
  <div className="min-w-0">
158
160
  <p className="text-[11px] font-medium text-foreground/70">
159
161
  {t('asset.parameter.relatedVulns')}
160
162
  </p>
161
163
  <div className="mt-1 flex flex-wrap gap-1.5">
162
- {parameter.related_vuln_ids.map((vulnId) => (
164
+ {relatedVulnIds.map((vulnId) => (
163
165
  <Button
164
166
  key={vulnId}
165
167
  variant="outline"
@@ -37,6 +37,7 @@ import { usePocErrorMessage } from '@renderer/components/poc/usePocErrorMessage'
37
37
  import { getApi } from '@renderer/lib/api'
38
38
  import { useI18n } from '@renderer/i18n'
39
39
  import { cn } from '@renderer/lib/utils'
40
+ import { getPortalContainer } from '../../../dsh/portal'
40
41
 
41
42
  interface PocConfigPanelProps {
42
43
  open: boolean
@@ -122,8 +123,8 @@ export function PocConfigPanel({
122
123
  : null
123
124
 
124
125
  return (
125
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
126
- <DialogPrimitive.Portal>
126
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
127
+ <DialogPrimitive.Portal container={getPortalContainer()}>
127
128
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[85] bg-[hsl(224_18%_10%/0.3)]" />
128
129
  <DialogPrimitive.Content
129
130
  data-testid="retest-config-panel"
@@ -5,6 +5,7 @@ import type { PocConfigView } from '@shared/poc'
5
5
  import { ProxySection } from '@renderer/components/poc/PocConfigPanel'
6
6
  import { useI18n } from '@renderer/i18n'
7
7
  import { getApi } from '@renderer/lib/api'
8
+ import { getPortalContainer } from '../../../dsh/portal'
8
9
 
9
10
  interface RetestProxyDialogProps {
10
11
  open: boolean
@@ -43,8 +44,8 @@ export function RetestProxyDialog({ open, onOpenChange }: RetestProxyDialogProps
43
44
  }, [open, t])
44
45
 
45
46
  return (
46
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
47
- <DialogPrimitive.Portal>
47
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
48
+ <DialogPrimitive.Portal container={getPortalContainer()}>
48
49
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[85] bg-[hsl(224_18%_10%/0.3)]" />
49
50
  <DialogPrimitive.Content className="sheet-content fixed left-1/2 top-1/2 z-[90] flex max-h-[88vh] w-[calc(100vw-32px)] max-w-[560px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-border bg-background">
50
51
  <div className="flex shrink-0 items-start justify-between gap-3 border-b border-border px-5 py-4">
@@ -6,6 +6,7 @@ import type { SnapshotSession } from '@shared/types'
6
6
  import { Button } from '@renderer/components/ui/button'
7
7
  import { apiUnavailableMessage, getApi, toErrorMessage } from '@renderer/lib/api'
8
8
  import { useI18n } from '@renderer/i18n'
9
+ import { getPortalContainer } from '../../../dsh/portal'
9
10
 
10
11
  interface DeleteSnapshotDialogProps {
11
12
  open: boolean
@@ -56,13 +57,14 @@ export function DeleteSnapshotDialog({
56
57
  }
57
58
 
58
59
  return (
59
- <DialogPrimitive.Root
60
+ <DialogPrimitive.Root
61
+ modal={false}
60
62
  open={open}
61
63
  onOpenChange={(next) => {
62
64
  if (!pending) onOpenChange(next)
63
65
  }}
64
66
  >
65
- <DialogPrimitive.Portal>
67
+ <DialogPrimitive.Portal container={getPortalContainer()}>
66
68
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
67
69
  <DialogPrimitive.Content className="fixed left-1/2 top-1/2 z-[70] w-[min(440px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
68
70
  <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
@@ -2,6 +2,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
2
2
  import { Loader2, Trash2 } from 'lucide-react'
3
3
  import { Button } from '@renderer/components/ui/button'
4
4
  import { useI18n } from '@renderer/i18n'
5
+ import { getPortalContainer } from '../../../dsh/portal'
5
6
 
6
7
  interface BulkDeleteDialogProps {
7
8
  open: boolean
@@ -22,8 +23,8 @@ export function BulkDeleteDialog({
22
23
  const { t } = useI18n()
23
24
 
24
25
  return (
25
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
26
- <DialogPrimitive.Portal>
26
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
27
+ <DialogPrimitive.Portal container={getPortalContainer()}>
27
28
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
28
29
  <DialogPrimitive.Content className="fixed left-1/2 top-1/2 z-[70] w-[min(420px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
29
30
  <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
@@ -2,6 +2,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
2
2
  import { Loader2, Trash2 } from 'lucide-react'
3
3
  import { Button } from '@renderer/components/ui/button'
4
4
  import { useI18n } from '@renderer/i18n'
5
+ import { getPortalContainer } from '../../../dsh/portal'
5
6
 
6
7
  interface ConfirmDeleteDialogProps {
7
8
  open: boolean
@@ -22,8 +23,8 @@ export function ConfirmDeleteDialog({
22
23
  const { t } = useI18n()
23
24
 
24
25
  return (
25
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
26
- <DialogPrimitive.Portal>
26
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
27
+ <DialogPrimitive.Portal container={getPortalContainer()}>
27
28
  <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
28
29
  <DialogPrimitive.Content className="fixed left-1/2 top-1/2 z-[70] w-[min(420px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
29
30
  <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
@@ -192,3 +192,121 @@ export function testParameterValuesText(values: readonly string[]): string {
192
192
  .filter((value) => value.length > 0)
193
193
  .join('\n')
194
194
  }
195
+
196
+ /* ------------------------------------------------------------------------- */
197
+ /* Defensive normalization for agent-written asset JSON */
198
+ /* ------------------------------------------------------------------------- */
199
+
200
+ /** Derive the query string / body from a captured raw HTTP request. */
201
+ export function extractDisplayParamsFromRaw(raw: string | undefined): string {
202
+ const source = (raw ?? '').replace(/\r\n/g, '\n').trim()
203
+ if (source.length === 0) return ''
204
+ const lines = source.split('\n')
205
+ const requestLine = lines[0] ?? ''
206
+ const queryIndex = requestLine.indexOf('?')
207
+ const query = queryIndex >= 0 ? requestLine.slice(queryIndex).trim() : ''
208
+ const separator = source.indexOf('\n\n')
209
+ const body = separator >= 0 ? source.slice(separator + 2).trim() : ''
210
+ return [query, body].filter((part) => part.length > 0).join('\n')
211
+ }
212
+
213
+ /**
214
+ * The Host normalizes asset rows before serving them, but cached responses from
215
+ * older plugin builds (or an MCP-era database) may still contain aliases such as
216
+ * `raw` / `param` / `where` / `example`. Normalizing again in the renderer keeps
217
+ * the drawer and the memory graph crash-free instead of blanking the panel.
218
+ */
219
+ export function normalizeDisplayInterfaces(raw: unknown): AssetInterface[] {
220
+ const list = Array.isArray(raw) ? raw : []
221
+ const result: AssetInterface[] = []
222
+ for (const value of list) {
223
+ if (typeof value === 'string') {
224
+ if (value.trim().length > 0) result.push({ path: value.trim() })
225
+ continue
226
+ }
227
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) continue
228
+ const item = value as Record<string, unknown>
229
+ const path = [item.path, item.url, item.route, item.endpoint]
230
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
231
+ .find((entry) => entry.length > 0)
232
+ if (!path) continue
233
+ const entry: AssetInterface = { path }
234
+ const method = typeof item.method === 'string' ? item.method.trim().toUpperCase() : ''
235
+ if ((INTERFACE_METHOD_STYLES as Record<string, string>)[method]) {
236
+ entry.method = method as AssetInterface['method']
237
+ }
238
+ const request = [item.request, item.raw, item.http]
239
+ .map((candidate) => (typeof candidate === 'string' ? candidate : ''))
240
+ .find((candidate) => candidate.trim().length > 0)
241
+ const params =
242
+ [item.params, item.parameters, item.query, item.data]
243
+ .map((candidate) => (typeof candidate === 'string' ? candidate.trim() : ''))
244
+ .find((candidate) => candidate.length > 0) || extractDisplayParamsFromRaw(request)
245
+ if (params) entry.params = params
246
+ if (request) entry.request = request
247
+ result.push(entry)
248
+ }
249
+ return result
250
+ }
251
+
252
+ /** Canonical test-parameter candidates from aliased agent records. */
253
+ export function normalizeDisplayTestParameters(raw: unknown): TestParameterCandidate[] {
254
+ const list = Array.isArray(raw) ? raw : []
255
+ const result: TestParameterCandidate[] = []
256
+ const seen = new Set<string>()
257
+ for (const value of list) {
258
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) continue
259
+ const item = value as Record<string, unknown>
260
+ const pickText = (names: string[]): string => {
261
+ for (const name of names) {
262
+ const candidate = item[name]
263
+ if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
264
+ }
265
+ return ''
266
+ }
267
+ const name = pickText(['name', 'param', 'key'])
268
+ const endpoint = pickText(['endpoint', 'interface', 'api', 'path'])
269
+ const rawValues = Array.isArray(item.values) ? item.values : []
270
+ const values = (rawValues.length > 0 ? rawValues : [pickText(['example', 'value', 'sample'])])
271
+ .filter((entry): entry is string | number => typeof entry === 'string' || typeof entry === 'number')
272
+ .map(String)
273
+ .slice(0, 50)
274
+ if (name.length === 0 && endpoint.length === 0 && values.length === 0) continue
275
+ const locationRaw = pickText(['location', 'where', 'in']).toLowerCase()
276
+ const locationAliases: Record<string, string> = {
277
+ json: 'body',
278
+ form: 'body',
279
+ formdata: 'body',
280
+ post: 'body',
281
+ querystring: 'query',
282
+ url: 'query',
283
+ headers: 'header',
284
+ cookies: 'cookie',
285
+ }
286
+ const mappedLocation = locationAliases[locationRaw] || locationRaw
287
+ const location = (
288
+ ['path', 'query', 'body', 'header', 'cookie'].includes(mappedLocation) ? mappedLocation : 'query'
289
+ ) as TestParameterCandidate['location']
290
+ const source = pickText(['source', 'file', 'interface', 'endpoint']) || endpoint || '(unknown)'
291
+ const key = `${endpoint}\u0000${name}\u0000${location}`
292
+ if (seen.has(key)) continue
293
+ seen.add(key)
294
+ const entry: TestParameterCandidate = {
295
+ name: name || 'value',
296
+ endpoint: endpoint || source || '(unknown)',
297
+ location,
298
+ values,
299
+ source,
300
+ tested: item.tested === true,
301
+ related_vuln_ids: Array.isArray(item.related_vuln_ids)
302
+ ? item.related_vuln_ids.filter((entry): entry is string => typeof entry === 'string')
303
+ : [],
304
+ }
305
+ const pattern = pickText(['pattern'])
306
+ if (pattern) entry.pattern = pattern
307
+ const notes = pickText(['notes', 'note'])
308
+ if (notes) entry.notes = notes
309
+ result.push(entry)
310
+ }
311
+ return result
312
+ }
@@ -21,6 +21,12 @@ import {
21
21
  sendJson,
22
22
  } from '../http.js'
23
23
  import { jsonArray, likePattern, text, vulnListItem } from './vulns.js'
24
+ import {
25
+ normalizeInterfaces,
26
+ normalizeLeaks,
27
+ normalizeProjectName,
28
+ normalizeTestParameters,
29
+ } from './normalize-asset.js'
24
30
 
25
31
  const ASSET_STATUSES = ['Alive', 'Dead', 'WAF_Blocked']
26
32
  const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
@@ -51,71 +57,22 @@ function bool01(value) {
51
57
  }
52
58
 
53
59
  /**
54
- * Normalize stored `interfaces` to the shared `AssetInterface[]` shape.
55
- * @param {unknown} raw
60
+ * Normalize stored asset JSON to the canonical shared shapes. Agent-written
61
+ * records may use aliases (raw/request, type/file, param/where/example); the
62
+ * shared normalizers keep the drawer and the memory graph crash-free.
56
63
  */
57
64
  function interfaceList(raw) {
58
- const out = []
59
- for (const item of jsonArray(raw)) {
60
- if (typeof item === 'string') {
61
- if (item.trim().length > 0) out.push({ path: item.trim() })
62
- continue
63
- }
64
- if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
65
- const path = text(item.path).trim()
66
- if (path.length === 0) continue
67
- /** @type {Record<string, any>} */
68
- const entry = { path }
69
- if (typeof item.method === 'string' && HTTP_METHODS.includes(item.method)) entry.method = item.method
70
- if (typeof item.params === 'string' && item.params.length > 0) entry.params = item.params
71
- if (typeof item.request === 'string' && item.request.length > 0) entry.request = item.request
72
- out.push(entry)
73
- }
74
- return out
65
+ return normalizeInterfaces(raw)
75
66
  }
76
67
 
77
68
  /** @param {unknown} raw */
78
69
  function leakedInfoList(raw) {
79
- const out = []
80
- for (const item of jsonArray(raw)) {
81
- if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
82
- const infoType = text(item.info_type ?? item.infoType)
83
- out.push({
84
- info_type: LEAKED_INFO_TYPES.includes(infoType) ? infoType : 'Other',
85
- value: text(item.value),
86
- source: text(item.source),
87
- potential_impact: text(item.potential_impact ?? item.potentialImpact),
88
- })
89
- }
90
- return out
70
+ return normalizeLeaks(raw)
91
71
  }
92
72
 
93
73
  /** @param {unknown} raw */
94
74
  function testParameterList(raw) {
95
- const out = []
96
- for (const item of jsonArray(raw)) {
97
- if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
98
- const location = text(item.location)
99
- /** @type {Record<string, any>} */
100
- const entry = {
101
- name: text(item.name),
102
- endpoint: text(item.endpoint),
103
- location: TEST_PARAMETER_LOCATIONS.includes(location) ? location : 'query',
104
- values: jsonArray(item.values)
105
- .filter((value) => typeof value === 'string' || typeof value === 'number')
106
- .map((value) => String(value)),
107
- source: text(item.source),
108
- tested: item.tested === true,
109
- }
110
- if (typeof item.pattern === 'string' && item.pattern.length > 0) entry.pattern = item.pattern
111
- if (typeof item.notes === 'string' && item.notes.length > 0) entry.notes = item.notes
112
- const related = jsonArray(item.related_vuln_ids ?? item.relatedVulnIds).filter(
113
- (value) => typeof value === 'string',
114
- )
115
- if (related.length > 0) entry.related_vuln_ids = related
116
- out.push(entry)
117
- }
118
- return out
75
+ return normalizeTestParameters(raw)
119
76
  }
120
77
 
121
78
  /**
@@ -153,7 +110,7 @@ function assetListItem(row) {
153
110
  return {
154
111
  host_port: text(source.host_port),
155
112
  host: text(source.host),
156
- project_name: text(source.project_name),
113
+ project_name: normalizeProjectName(text(source.project_name)),
157
114
  tech_stack: text(source.tech_stack),
158
115
  auth_required: bool01(source.auth_required),
159
116
  status: ASSET_STATUSES.includes(text(source.status)) ? text(source.status) : 'Alive',
@@ -173,7 +130,7 @@ function assetContext(row) {
173
130
  return {
174
131
  host_port: text(source.host_port),
175
132
  host: text(source.host),
176
- project_name: text(source.project_name),
133
+ project_name: normalizeProjectName(text(source.project_name)),
177
134
  tech_stack: text(source.tech_stack),
178
135
  auth_required: bool01(source.auth_required),
179
136
  status: ASSET_STATUSES.includes(text(source.status)) ? text(source.status) : 'Alive',