Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add threat feed command #195

Merged
merged 8 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .dep-stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"bundle-name": "^4.1.0",
"camelcase": "^8.0.0",
"chalk": "^5.3.0",
"cli-cursor": "^4.0.0",
"cli-cursor": "^5.0.0",
"configstore": "^7.0.0",
"default-browser": "^5.2.1",
"default-browser-id": "^5.0.0",
Expand Down Expand Up @@ -56,7 +56,9 @@
"latest-version": "^9.0.0",
"log-symbols": "^6.0.0",
"meow": "^13.2.0",
"mimic-function": "^5.0.0",
"npm-run-path": "^5.2.0",
"onetime": "^5.1.0",
"open": "^10.1.0",
"ora": "^8.0.1",
"package-json": "^10.0.0",
Expand All @@ -66,11 +68,11 @@
"pretty-ms": "^9.0.0",
"pupa": "^3.1.0",
"registry-url": "^6.0.1",
"restore-cursor": "^4.0.0",
"restore-cursor": "^5.0.0",
"run-applescript": "^7.0.0",
"semver-diff": "^4.0.0",
"slash": "^5.1.0",
"stdin-discarder": "^0.2.1",
"stdin-discarder": "^0.2.2",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0",
"strip-final-newline": "^4.0.0",
Expand Down Expand Up @@ -98,7 +100,7 @@
"cli-spinners": "^2.9.2",
"cross-spawn": "^7.0.3",
"dot-prop": "^9.0.0",
"eastasianwidth": "^0.2.0",
"eastasianwidth": "^0.3.0",
"emoji-regex": "^10.3.0",
"fast-glob": "^3.3.2",
"graceful-fs": "^4.2.6",
Expand Down Expand Up @@ -127,7 +129,7 @@
"cli-spinners": "^2.9.2",
"cross-spawn": "^7.0.3",
"dot-prop": "^9.0.0",
"eastasianwidth": "^0.2.0",
"eastasianwidth": "^0.3.0",
"emoji-regex": "^10.3.0",
"fast-glob": "^3.3.2",
"graceful-fs": "^4.2.6",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/diff-scan/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import chalk from 'chalk'

Check warning on line 1 in src/commands/diff-scan/get.ts

View workflow job for this annotation

GitHub Actions / Linting / Test (20, ubuntu-latest)

There should be at least one empty line between import groups
import fs from 'fs'
import meow from 'meow'
import ora from 'ora'
Expand All @@ -11,7 +11,7 @@
import type { CliSubcommand } from '../../utils/meow-with-subcommands'
import type { Ora } from 'ora'
import { AuthError } from '../../utils/errors'
import { handleAPIError, queryAPI } from '../../utils/api-helpers'
import { handleAPIError, queryOrgsAPI } from '../../utils/api-helpers'

export const get: CliSubcommand = {
description: 'Get a diff scan for an organization',
Expand Down Expand Up @@ -142,7 +142,7 @@
spinner: Ora,
apiKey: string,
): Promise<void> {
const response = await queryAPI(`${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, apiKey)
const response = await queryOrgsAPI(`${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, apiKey)
const data = await response.json();

if(!response.ok){
Expand Down
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './repos'
export * from './dependencies'
export * from './analytics'
export * from './diff-scan'
export * from './threat-feed'
207 changes: 207 additions & 0 deletions src/commands/threat-feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/* Not a fan of adding the no-check, mainly doing it because
the types associated with the blessed packages
create some type errors
*/
// @ts-nocheck
// @ts-ignore
import blessed from 'blessed'
// @ts-ignore
import contrib from 'blessed-contrib'
import meow from 'meow'
import ora from 'ora'

import { outputFlags } from '../flags'
import { printFlagList } from '../utils/formatting'
import { getDefaultKey } from '../utils/sdk'

import type { CliSubcommand } from '../utils/meow-with-subcommands'
import type { Ora } from 'ora'
import { AuthError } from '../utils/errors'
import { queryAPI } from '../utils/api-helpers'

export const threatFeed: CliSubcommand = {
description: 'Look up the threat feed',
async run(argv, importMeta, { parentName }) {
const name = parentName + ' threat-feed'

const input = setupCommand(name, threatFeed.description, argv, importMeta)
if (input) {
const apiKey = getDefaultKey()
if(!apiKey){
throw new AuthError("User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.")
}
const spinner = ora(`Looking up the threat feed \n`).start()
await fetchThreatFeed(input, spinner, apiKey)
}
}
}

const threatFeedFlags = {
perPage: {
type: 'number',
shortFlag: 'pp',
default: 30,
description: 'Number of items per page'
},
page: {
type: 'string',
shortFlag: 'p',
default: '1',
description: 'Page token'
},
direction: {
type: 'string',
shortFlag: 'd',
default: 'desc',
description: 'Order asc or desc by the createdAt attribute.'
},
filter: {
type: 'string',
shortFlag: 'f',
default: 'mal',
description: 'Filter what type of threats to return'
}
}

// Internal functions

type CommandContext = {
outputJson: boolean
outputMarkdown: boolean
per_page: number
page: string
direction: string
filter: string
}

function setupCommand(
name: string,
description: string,
argv: readonly string[],
importMeta: ImportMeta
): CommandContext | undefined {
const flags: { [key: string]: any } = {
...threatFeedFlags,
...outputFlags
}

const cli = meow(
`
Usage
$ ${name}

Options
${printFlagList(flags, 6)}

Examples
$ ${name}
$ ${name} --perPage=5 --page=2 --direction=asc --filter=joke
`,
{
argv,
description,
importMeta,
flags
}
)

const {
json: outputJson,
markdown: outputMarkdown,
perPage: per_page,
page,
direction,
filter
} = cli.flags

return <CommandContext>{
outputJson,
outputMarkdown,
per_page,
page,
direction,
filter
}
}

type ThreatResult = {
createdAt: string
description: string
id: number,
locationHtmlUrl: string
packageHtmlUrl: string
purl: string
removedAt: string
threatType: string
}

async function fetchThreatFeed(
{ per_page, page, direction, filter, outputJson }: CommandContext,
spinner: Ora,
apiKey: string
): Promise<void> {
const formattedQueryParams = formatQueryParams({ per_page, page, direction, filter }).join('&')

const response = await queryAPI(`threat-feed?${formattedQueryParams}`, apiKey)
const data: {results: ThreatResult[], nextPage: string} = await response.json();

spinner.stop()

if(outputJson){
return console.log(data)
}

const screen = blessed.screen()

var table = contrib.table({
keys: 'true',
fg: 'white',
selectedFg: 'white',
selectedBg: 'magenta',
interactive: 'true',
label: 'Threat feed',
width: '100%',
height: '100%',
border: {
type: "line",
fg: "cyan"
},
columnSpacing: 3, //in chars
columnWidth: [9, 30, 10, 17, 13, 100] /*in chars*/
})

// allow control the table with the keyboard
table.focus()

screen.append(table)

const formattedOutput = formatResults(data.results)

table.setData({ headers: ['Ecosystem', 'Name', 'Version', 'Threat type', 'Detected at', 'Details'], data: formattedOutput })

screen.render()

screen.key(['escape', 'q', 'C-c'], () => process.exit(0))
}

const formatResults = (data: ThreatResult[]) => {
return data.map(d => {
const ecosystem = d.purl.split('pkg:')[1].split('/')[0]
const name = d.purl.split('/')[1].split('@')[0]
const version = d.purl.split('@')[1]

const timeStart = new Date(d.createdAt);
const timeEnd = new Date()

const diff = getHourDiff(timeStart, timeEnd)
const hourDiff = diff > 0 ? `${diff} hours ago` : `${getMinDiff(timeStart, timeEnd)} minutes ago`

return [ecosystem, decodeURIComponent(name), version, d.threatType, hourDiff, d.locationHtmlUrl]
})
}

const formatQueryParams = (params: any) => Object.entries(params).map(entry => `${entry[0]}=${entry[1]}`)

const getHourDiff = (start, end) => Math.floor((end - start) / 3600000)

const getMinDiff = (start, end) => Math.floor((end - start) / 60000)
11 changes: 10 additions & 1 deletion src/utils/api-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,20 @@ export async function handleAPIError(code: number) {

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

export async function queryAPI(path: string, apiKey: string) {
export async function queryOrgsAPI(path: string, apiKey: string) {
return await fetch(`${API_V0_URL}/orgs/${path}`, {
method: 'GET',
headers: {
'Authorization': 'Basic ' + btoa(`${apiKey}:${apiKey}`)
}
});
}

export async function queryAPI(path: string, apiKey: string) {
return await fetch(`${API_V0_URL}/${path}`, {
method: 'GET',
headers: {
'Authorization': 'Basic ' + btoa(`${apiKey}:${apiKey}`)
}
});
}
Loading