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