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