Skip to content

Commit 969df5a

Browse files
Merge pull request #195 from SocketDev/cg/addThreatFeedCommand
Add threat feed command
2 parents 14999af + 4957745 commit 969df5a

File tree

5 files changed

+227
-8
lines changed

5 files changed

+227
-8
lines changed

.dep-stats.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"bundle-name": "^4.1.0",
2828
"camelcase": "^8.0.0",
2929
"chalk": "^5.3.0",
30-
"cli-cursor": "^4.0.0",
30+
"cli-cursor": "^5.0.0",
3131
"configstore": "^7.0.0",
3232
"default-browser": "^5.2.1",
3333
"default-browser-id": "^5.0.0",
@@ -56,7 +56,9 @@
5656
"latest-version": "^9.0.0",
5757
"log-symbols": "^6.0.0",
5858
"meow": "^13.2.0",
59+
"mimic-function": "^5.0.0",
5960
"npm-run-path": "^5.2.0",
61+
"onetime": "^5.1.0",
6062
"open": "^10.1.0",
6163
"ora": "^8.0.1",
6264
"package-json": "^10.0.0",
@@ -66,11 +68,11 @@
6668
"pretty-ms": "^9.0.0",
6769
"pupa": "^3.1.0",
6870
"registry-url": "^6.0.1",
69-
"restore-cursor": "^4.0.0",
71+
"restore-cursor": "^5.0.0",
7072
"run-applescript": "^7.0.0",
7173
"semver-diff": "^4.0.0",
7274
"slash": "^5.1.0",
73-
"stdin-discarder": "^0.2.1",
75+
"stdin-discarder": "^0.2.2",
7476
"string-width": "^7.0.0",
7577
"strip-ansi": "^7.1.0",
7678
"strip-final-newline": "^4.0.0",
@@ -98,7 +100,7 @@
98100
"cli-spinners": "^2.9.2",
99101
"cross-spawn": "^7.0.3",
100102
"dot-prop": "^9.0.0",
101-
"eastasianwidth": "^0.2.0",
103+
"eastasianwidth": "^0.3.0",
102104
"emoji-regex": "^10.3.0",
103105
"fast-glob": "^3.3.2",
104106
"graceful-fs": "^4.2.6",
@@ -127,7 +129,7 @@
127129
"cli-spinners": "^2.9.2",
128130
"cross-spawn": "^7.0.3",
129131
"dot-prop": "^9.0.0",
130-
"eastasianwidth": "^0.2.0",
132+
"eastasianwidth": "^0.3.0",
131133
"emoji-regex": "^10.3.0",
132134
"fast-glob": "^3.3.2",
133135
"graceful-fs": "^4.2.6",

src/commands/diff-scan/get.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getDefaultKey } from '../../utils/sdk'
1111
import type { CliSubcommand } from '../../utils/meow-with-subcommands'
1212
import type { Ora } from 'ora'
1313
import { AuthError } from '../../utils/errors'
14-
import { handleAPIError, queryAPI } from '../../utils/api-helpers'
14+
import { handleAPIError, queryOrgsAPI } from '../../utils/api-helpers'
1515

1616
export const get: CliSubcommand = {
1717
description: 'Get a diff scan for an organization',
@@ -142,7 +142,7 @@ async function getDiffScan(
142142
spinner: Ora,
143143
apiKey: string,
144144
): Promise<void> {
145-
const response = await queryAPI(`${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, apiKey)
145+
const response = await queryOrgsAPI(`${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, apiKey)
146146
const data = await response.json();
147147

148148
if(!response.ok){

src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './repos'
1515
export * from './dependencies'
1616
export * from './analytics'
1717
export * from './diff-scan'
18+
export * from './threat-feed'

src/commands/threat-feed.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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)

src/utils/api-helpers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,20 @@ export async function handleAPIError(code: number) {
5656

5757
const API_V0_URL = 'https://api.socket.dev/v0'
5858

59-
export async function queryAPI(path: string, apiKey: string) {
59+
export async function queryOrgsAPI(path: string, apiKey: string) {
6060
return await fetch(`${API_V0_URL}/orgs/${path}`, {
6161
method: 'GET',
6262
headers: {
6363
'Authorization': 'Basic ' + btoa(`${apiKey}:${apiKey}`)
6464
}
6565
});
66+
}
67+
68+
export async function queryAPI(path: string, apiKey: string) {
69+
return await fetch(`${API_V0_URL}/${path}`, {
70+
method: 'GET',
71+
headers: {
72+
'Authorization': 'Basic ' + btoa(`${apiKey}:${apiKey}`)
73+
}
74+
});
6675
}

0 commit comments

Comments
 (0)