1
+ /* Not a fan of adding the no-check, mainly doing it because
2
+ the types associated with the blessed packages
3
+ create some type errors
4
+ */
5
+ // @ts-nocheck
6
+ // @ts-ignore
7
+ import blessed from 'blessed'
8
+ // @ts-ignore
9
+ import contrib from 'blessed-contrib'
10
+ import meow from 'meow'
11
+ import ora from 'ora'
12
+
13
+ import { outputFlags } from '../flags'
14
+ import { printFlagList } from '../utils/formatting'
15
+ import { getDefaultKey } from '../utils/sdk'
16
+
17
+ import type { CliSubcommand } from '../utils/meow-with-subcommands'
18
+ import type { Ora } from 'ora'
19
+ import { AuthError } from '../utils/errors'
20
+ import { queryAPI } from '../utils/api-helpers'
21
+
22
+ export const threatFeed: CliSubcommand = {
23
+ description: 'Look up the threat feed',
24
+ async run(argv, importMeta, { parentName }) {
25
+ const name = parentName + ' threat-feed'
26
+
27
+ const input = setupCommand(name, threatFeed.description, argv, importMeta)
28
+ if (input) {
29
+ const apiKey = getDefaultKey()
30
+ if(!apiKey){
31
+ throw new AuthError("User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.")
32
+ }
33
+ const spinner = ora(`Looking up the threat feed \n`).start()
34
+ await fetchThreatFeed(input, spinner, apiKey)
35
+ }
36
+ }
37
+ }
38
+
39
+ const threatFeedFlags = {
40
+ perPage: {
41
+ type: 'number',
42
+ shortFlag: 'pp',
43
+ default: 30,
44
+ description: 'Number of items per page'
45
+ },
46
+ page: {
47
+ type: 'string',
48
+ shortFlag: 'p',
49
+ default: '1',
50
+ description: 'Page token'
51
+ },
52
+ direction: {
53
+ type: 'string',
54
+ shortFlag: 'd',
55
+ default: 'desc',
56
+ description: 'Order asc or desc by the createdAt attribute.'
57
+ },
58
+ filter: {
59
+ type: 'string',
60
+ shortFlag: 'f',
61
+ default: 'mal',
62
+ description: 'Filter what type of threats to return'
63
+ }
64
+ }
65
+
66
+ // Internal functions
67
+
68
+ type CommandContext = {
69
+ outputJson: boolean
70
+ outputMarkdown: boolean
71
+ per_page: number
72
+ page: string
73
+ direction: string
74
+ filter: string
75
+ }
76
+
77
+ function setupCommand(
78
+ name: string,
79
+ description: string,
80
+ argv: readonly string[],
81
+ importMeta: ImportMeta
82
+ ): CommandContext | undefined {
83
+ const flags: { [key: string]: any } = {
84
+ ...threatFeedFlags,
85
+ ...outputFlags
86
+ }
87
+
88
+ const cli = meow(
89
+ `
90
+ Usage
91
+ $ ${name}
92
+
93
+ Options
94
+ ${printFlagList(flags, 6)}
95
+
96
+ Examples
97
+ $ ${name}
98
+ $ ${name} --perPage=5 --page=2 --direction=asc --filter=joke
99
+ `,
100
+ {
101
+ argv,
102
+ description,
103
+ importMeta,
104
+ flags
105
+ }
106
+ )
107
+
108
+ const {
109
+ json: outputJson,
110
+ markdown: outputMarkdown,
111
+ perPage: per_page,
112
+ page,
113
+ direction,
114
+ filter
115
+ } = cli.flags
116
+
117
+ return <CommandContext>{
118
+ outputJson,
119
+ outputMarkdown,
120
+ per_page,
121
+ page,
122
+ direction,
123
+ filter
124
+ }
125
+ }
126
+
127
+ type ThreatResult = {
128
+ createdAt: string
129
+ description: string
130
+ id: number,
131
+ locationHtmlUrl: string
132
+ packageHtmlUrl: string
133
+ purl: string
134
+ removedAt: string
135
+ threatType: string
136
+ }
137
+
138
+ async function fetchThreatFeed(
139
+ { per_page, page, direction, filter, outputJson }: CommandContext,
140
+ spinner: Ora,
141
+ apiKey: string
142
+ ): Promise<void> {
143
+ const formattedQueryParams = formatQueryParams({ per_page, page, direction, filter }).join('&')
144
+
145
+ const response = await queryAPI(`threat-feed?${formattedQueryParams}`, apiKey)
146
+ const data: {results: ThreatResult[], nextPage: string} = await response.json();
147
+
148
+ spinner.stop()
149
+
150
+ if(outputJson){
151
+ return console.log(data)
152
+ }
153
+
154
+ const screen = blessed.screen()
155
+
156
+ var table = contrib.table({
157
+ keys: 'true',
158
+ fg: 'white',
159
+ selectedFg: 'white',
160
+ selectedBg: 'magenta',
161
+ interactive: 'true',
162
+ label: 'Threat feed',
163
+ width: '100%',
164
+ height: '100%',
165
+ border: {
166
+ type: "line",
167
+ fg: "cyan"
168
+ },
169
+ columnSpacing: 3, //in chars
170
+ columnWidth: [9, 30, 10, 17, 13, 100] /*in chars*/
171
+ })
172
+
173
+ // allow control the table with the keyboard
174
+ table.focus()
175
+
176
+ screen.append(table)
177
+
178
+ const formattedOutput = formatResults(data.results)
179
+
180
+ table.setData({ headers: ['Ecosystem', 'Name', 'Version', 'Threat type', 'Detected at', 'Details'], data: formattedOutput })
181
+
182
+ screen.render()
183
+
184
+ screen.key(['escape', 'q', 'C-c'], () => process.exit(0))
185
+ }
186
+
187
+ const formatResults = (data: ThreatResult[]) => {
188
+ return data.map(d => {
189
+ const ecosystem = d.purl.split('pkg:')[1].split('/')[0]
190
+ const name = d.purl.split('/')[1].split('@')[0]
191
+ const version = d.purl.split('@')[1]
192
+
193
+ const timeStart = new Date(d.createdAt);
194
+ const timeEnd = new Date()
195
+
196
+ const diff = getHourDiff(timeStart, timeEnd)
197
+ const hourDiff = diff > 0 ? `${diff} hours ago` : `${getMinDiff(timeStart, timeEnd)} minutes ago`
198
+
199
+ return [ecosystem, decodeURIComponent(name), version, d.threatType, hourDiff, d.locationHtmlUrl]
200
+ })
201
+ }
202
+
203
+ const formatQueryParams = (params: any) => Object.entries(params).map(entry => `${entry[0]}=${entry[1]}`)
204
+
205
+ const getHourDiff = (start, end) => Math.floor((end - start) / 3600000)
206
+
207
+ const getMinDiff = (start, end) => Math.floor((end - start) / 60000)
0 commit comments