Skip to content

Commit d980864

Browse files
committed
Improve UX for threat-feed and audit-logs
1 parent d53956e commit d980864

File tree

3 files changed

+126
-60
lines changed

3 files changed

+126
-60
lines changed

src/commands/audit-log/output-audit-log.mts

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
66
import constants from '../../constants.mts'
77
import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts'
88
import { mdTable } from '../../utils/markdown.mts'
9+
import { msAtHome } from '../../utils/ms-at-home.mts'
910
import { serializeResultJson } from '../../utils/serialize-result-json.mts'
1011

1112
import type { CResult, OutputKind } from '../../types.mts'
@@ -67,8 +68,8 @@ export async function outputAuditLog(
6768
const filteredLogs = result.data.results
6869
const formattedOutput = filteredLogs.map(logs => [
6970
logs.event_id ?? '',
70-
logs.created_at ?? '',
71-
(logs.type || '').padEnd(30, ' '),
71+
msAtHome(logs.created_at ?? ''),
72+
logs.type ?? '',
7273
logs.user_email ?? '',
7374
logs.ip_address ?? '',
7475
logs.user_agent ?? '',
@@ -95,6 +96,16 @@ export async function outputAuditLog(
9596
screen.key(['escape', 'q', 'C-c'], () => process.exit(0))
9697

9798
const TableWidget = require('blessed-contrib/lib/widget/table.js')
99+
const tipsBoxHeight = 1 // 1 row for tips box
100+
const detailsBoxHeight = 20 // bottom N rows for details box. 20 gives 4 lines for condensed payload before it scrolls out of view
101+
102+
const maxWidths = [10, 10, 10, 10, 10, 10]
103+
formattedOutput.forEach(row => {
104+
row.forEach((str, i) => {
105+
maxWidths[i] = Math.max(str.length, maxWidths[i] ?? str.length)
106+
})
107+
})
108+
98109
const table: any = new TableWidget({
99110
keys: 'true',
100111
fg: 'white',
@@ -103,31 +114,40 @@ export async function outputAuditLog(
103114
interactive: 'true',
104115
label: `Audit Logs for ${orgSlug}`,
105116
width: '100%',
106-
height: '70%', // Changed from 100% to 70%
117+
top: 0,
118+
bottom: detailsBoxHeight + tipsBoxHeight,
107119
border: {
108120
type: 'line',
109121
fg: 'cyan',
110122
},
111-
columnWidth: [10, 30, 30, 25, 15, 200],
112-
// TODO: the truncation doesn't seem to work too well yet but when we add
113-
// `pad` alignment fails, when we extend columnSpacing alignment fails
114-
columnSpacing: 1,
123+
columnWidth: maxWidths, //[10, 30, 40, 25, 15, 200],
124+
// Note: spacing works as long as you don't reserve more than total width
125+
columnSpacing: 4,
115126
truncate: '_',
116127
})
117128

118-
// Create details box at the bottom
119129
const BoxWidget = require('blessed/lib/widgets/box.js')
130+
const tipsBox: Widgets.BoxElement = new BoxWidget({
131+
bottom: detailsBoxHeight, // sits just above the details box
132+
height: tipsBoxHeight,
133+
width: '100%',
134+
style: {
135+
fg: 'yellow',
136+
bg: 'black',
137+
},
138+
tags: true,
139+
content: '↑/↓: Move Enter: Select q/ESC: Quit',
140+
})
120141
const detailsBox: Widgets.BoxElement = new BoxWidget({
121142
bottom: 0,
122-
height: '30%',
143+
height: detailsBoxHeight,
123144
width: '100%',
124145
border: {
125146
type: 'line',
126147
fg: 'cyan',
127148
},
128149
label: 'Details',
129-
content:
130-
'Use arrow keys to navigate. Press Enter to select an event. Press q to exit.',
150+
content: formatResult(filteredLogs[0], true),
131151
style: {
132152
fg: 'white',
133153
},
@@ -141,28 +161,18 @@ export async function outputAuditLog(
141161
// allow control the table with the keyboard
142162
table.focus()
143163

164+
// Stacking order: table (top), tipsBox (middle), detailsBox (bottom)
144165
screen.append(table)
166+
screen.append(tipsBox)
145167
screen.append(detailsBox)
146168

147169
// Update details box when selection changes
148170
table.rows.on('select item', () => {
149171
const selectedIndex = table.rows.selected
150172
if (selectedIndex !== undefined && selectedIndex >= 0) {
151173
const selectedRow = filteredLogs[selectedIndex]
152-
if (selectedRow) {
153-
// Format the object with spacing but keep the payload compact because
154-
// that can contain just about anything and spread many lines.
155-
const obj = { ...selectedRow, payload: 'REPLACEME' }
156-
const json = JSON.stringify(obj, null, 2)
157-
.replace(
158-
/"payload": "REPLACEME"/,
159-
`"payload": ${JSON.stringify(selectedRow.payload ?? {})}`,
160-
)
161-
.replace(/^\s*"([^"]+)?"/gm, ' $1')
162-
// Note: the spacing works around issues with the table; it refuses to pad!
163-
detailsBox.setContent(json)
164-
screen.render()
165-
}
174+
detailsBox.setContent(formatResult(selectedRow))
175+
screen.render()
166176
}
167177
})
168178

@@ -172,10 +182,32 @@ export async function outputAuditLog(
172182
const selectedIndex = table.rows.selected
173183
screen.destroy()
174184
const selectedRow = formattedOutput[selectedIndex]
175-
logger.log('Last selection:\n', selectedRow)
185+
? formatResult(filteredLogs[selectedIndex], true)
186+
: '(none)'
187+
logger.log(`Last selection:\n${selectedRow.trim()}`)
176188
})
177189
}
178190

191+
function formatResult(
192+
selectedRow?: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'][number],
193+
keepQuotes = false,
194+
): string {
195+
if (!selectedRow) {
196+
return '(none)'
197+
}
198+
// Format the object with spacing but keep the payload compact because
199+
// that can contain just about anything and spread many lines.
200+
const obj = { ...selectedRow, payload: 'REPLACEME' }
201+
const json = JSON.stringify(obj, null, 2).replace(
202+
/"payload": "REPLACEME"/,
203+
`"payload": ${JSON.stringify(selectedRow.payload ?? {})}`,
204+
)
205+
if (keepQuotes) {
206+
return json
207+
}
208+
return json.replace(/^\s*"([^"]+)?"/gm, ' $1')
209+
}
210+
179211
export async function outputAsJson(
180212
auditLogs: CResult<SocketSdkReturnType<'getAuditLogEvents'>['data']>,
181213
{

src/commands/threat-feed/output-threat-feed.mts

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
44

55
import constants from '../../constants.mts'
66
import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts'
7+
import { msAtHome } from '../../utils/ms-at-home.mts'
78
import { serializeResultJson } from '../../utils/serialize-result-json.mts'
89

910
import type { ThreadFeedResponse, ThreatResult } from './types.mts'
@@ -50,6 +51,9 @@ export async function outputThreatFeed(
5051
screen.key(['escape', 'q', 'C-c'], () => process.exit(0))
5152

5253
const TableWidget = require('blessed-contrib/lib/widget/table.js')
54+
const detailsBoxHeight = 20 // bottom N rows for details box
55+
const tipsBoxHeight = 1 // 1 row for tips box
56+
5357
const table: any = new TableWidget({
5458
keys: 'true',
5559
fg: 'white',
@@ -58,7 +62,8 @@ export async function outputThreatFeed(
5862
interactive: 'true',
5963
label: 'Threat feed',
6064
width: '100%',
61-
height: '70%', // Changed from 100% to 70%
65+
top: 0,
66+
bottom: detailsBoxHeight + tipsBoxHeight,
6267
border: {
6368
type: 'line',
6469
fg: 'cyan',
@@ -70,11 +75,21 @@ export async function outputThreatFeed(
7075
truncate: '_',
7176
})
7277

73-
// Create details box at the bottom
7478
const BoxWidget = require('blessed/lib/widgets/box.js')
79+
const tipsBox: Widgets.BoxElement = new BoxWidget({
80+
bottom: detailsBoxHeight, // sits just above the details box
81+
height: tipsBoxHeight,
82+
width: '100%',
83+
style: {
84+
fg: 'yellow',
85+
bg: 'black',
86+
},
87+
tags: true,
88+
content: '↑/↓: Move Enter: Select q/ESC: Quit',
89+
})
7590
const detailsBox: Widgets.BoxElement = new BoxWidget({
7691
bottom: 0,
77-
height: '30%',
92+
height: detailsBoxHeight,
7893
width: '100%',
7994
border: {
8095
type: 'line',
@@ -100,10 +115,20 @@ export async function outputThreatFeed(
100115
data: formattedOutput,
101116
})
102117

118+
// Initialize details box with the first selection if available
119+
if (formattedOutput.length > 0) {
120+
const selectedRow = formattedOutput[0]
121+
if (selectedRow) {
122+
detailsBox.setContent(formatDetailBox(selectedRow, descriptions, 0))
123+
}
124+
}
125+
103126
// allow control the table with the keyboard
104127
table.focus()
105128

129+
// Stacking order: table (top), tipsBox (middle), detailsBox (bottom)
106130
screen.append(table)
131+
screen.append(tipsBox)
107132
screen.append(detailsBox)
108133

109134
// Update details box when selection changes
@@ -114,13 +139,7 @@ export async function outputThreatFeed(
114139
if (selectedRow) {
115140
// Note: the spacing works around issues with the table; it refuses to pad!
116141
detailsBox.setContent(
117-
`Ecosystem: ${selectedRow[0]}\n` +
118-
`Name: ${selectedRow[1]}\n` +
119-
`Version:${selectedRow[2]}\n` +
120-
`Threat type:${selectedRow[3]}\n` +
121-
`Detected at:${selectedRow[4]}\n` +
122-
`Details: ${selectedRow[5]}\n` +
123-
`Description: ${descriptions[selectedIndex]}`,
142+
formatDetailBox(selectedRow, descriptions, selectedIndex),
124143
)
125144
screen.render()
126145
}
@@ -137,6 +156,22 @@ export async function outputThreatFeed(
137156
})
138157
}
139158

159+
function formatDetailBox(
160+
selectedRow: string[],
161+
descriptions: string[],
162+
selectedIndex: number,
163+
): string {
164+
return (
165+
`Ecosystem: ${selectedRow[0]?.trim()}\n` +
166+
`Name: ${selectedRow[1]?.trim()}\n` +
167+
`Version: ${selectedRow[2]?.trim()}\n` +
168+
`Threat type: ${selectedRow[3]?.trim()}\n` +
169+
`Detected at: ${selectedRow[4]?.trim()}\n` +
170+
`Details: ${selectedRow[5]?.trim()}\n` +
171+
`Description: ${descriptions[selectedIndex]?.trim()}`
172+
)
173+
}
174+
140175
function formatResults(data: ThreatResult[]) {
141176
return data.map(d => {
142177
const ecosystem = d.purl.split('pkg:')[1]!.split('/')[0]!
@@ -156,27 +191,3 @@ function formatResults(data: ThreatResult[]) {
156191
]
157192
})
158193
}
159-
160-
function msAtHome(isoTimeStamp: string): string {
161-
const timeStart = Date.parse(isoTimeStamp)
162-
const timeEnd = Date.now()
163-
164-
const rtf = new Intl.RelativeTimeFormat('en', {
165-
numeric: 'always',
166-
style: 'short',
167-
})
168-
169-
const delta = timeEnd - timeStart
170-
if (delta < 60 * 60 * 1000) {
171-
return rtf.format(-Math.round(delta / (60 * 1000)), 'minute')
172-
// return Math.round(delta / (60 * 1000)) + ' min ago'
173-
} else if (delta < 24 * 60 * 60 * 1000) {
174-
return rtf.format(-(delta / (60 * 60 * 1000)).toFixed(1), 'hour')
175-
// return (delta / (60 * 60 * 1000)).toFixed(1) + ' hr ago'
176-
} else if (delta < 7 * 24 * 60 * 60 * 1000) {
177-
return rtf.format(-(delta / (24 * 60 * 60 * 1000)).toFixed(1), 'day')
178-
// return (delta / (24 * 60 * 60 * 1000)).toFixed(1) + ' day ago'
179-
} else {
180-
return isoTimeStamp.slice(0, 10)
181-
}
182-
}

src/utils/ms-at-home.mts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export function msAtHome(isoTimeStamp: string): string {
2+
const timeStart = Date.parse(isoTimeStamp)
3+
const timeEnd = Date.now()
4+
5+
const rtf = new Intl.RelativeTimeFormat('en', {
6+
numeric: 'always',
7+
style: 'short',
8+
})
9+
10+
const delta = timeEnd - timeStart
11+
if (delta < 60 * 60 * 1000) {
12+
return rtf.format(-Math.round(delta / (60 * 1000)), 'minute')
13+
// return Math.round(delta / (60 * 1000)) + ' min ago'
14+
} else if (delta < 24 * 60 * 60 * 1000) {
15+
return rtf.format(-(delta / (60 * 60 * 1000)).toFixed(1), 'hour')
16+
// return (delta / (60 * 60 * 1000)).toFixed(1) + ' hr ago'
17+
} else if (delta < 7 * 24 * 60 * 60 * 1000) {
18+
return rtf.format(-(delta / (24 * 60 * 60 * 1000)).toFixed(1), 'day')
19+
// return (delta / (24 * 60 * 60 * 1000)).toFixed(1) + ' day ago'
20+
} else {
21+
return isoTimeStamp.slice(0, 10)
22+
}
23+
}

0 commit comments

Comments
 (0)