From 794d71e5f0a6efa71124f6786a02e6466a698c70 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 30 Mar 2025 11:39:20 -0400 Subject: [PATCH 1/3] Update sdk --- .dep-stats.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.dep-stats.json b/.dep-stats.json index 52e7e5536..2d2fd89c3 100644 --- a/.dep-stats.json +++ b/.dep-stats.json @@ -10,7 +10,7 @@ "@socketregistry/packageurl-js": "1.0.4", "@socketsecurity/config": "2.1.3", "@socketsecurity/registry": "1.0.135", - "@socketsecurity/sdk": "1.4.16", + "@socketsecurity/sdk": "1.4.17", "blessed": "0.1.81", "blessed-contrib": "4.11.0", "browserslist": "4.24.4", diff --git a/package-lock.json b/package-lock.json index 6401d74e3..e532bbf21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@socketregistry/packageurl-js": "1.0.4", "@socketsecurity/config": "2.1.3", "@socketsecurity/registry": "1.0.135", - "@socketsecurity/sdk": "1.4.16", + "@socketsecurity/sdk": "1.4.17", "blessed": "0.1.81", "blessed-contrib": "4.11.0", "browserslist": "4.24.4", @@ -5214,9 +5214,9 @@ } }, "node_modules/@socketsecurity/sdk": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.4.16.tgz", - "integrity": "sha512-Wd0wbwQUe63s8Y+4meYkeO8rFXzSqVpFTm0YNb4fimtHOWhVC+24a7oLoxRmgjRaQMqhDgQIcj60s1Gr+5MFPg==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.4.17.tgz", + "integrity": "sha512-hiSSDTVLOfeMLUH+hgwG5c/N3e17gPJlrgRkHjucmsLd+1I0B2bwnhrR4yr/NeiJGKkaCRjf1ng714JnoFZsKA==", "license": "MIT", "dependencies": { "@socketsecurity/registry": "1.0.135" diff --git a/package.json b/package.json index 13d59d299..8c7d8aa69 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@socketregistry/packageurl-js": "1.0.4", "@socketsecurity/config": "2.1.3", "@socketsecurity/registry": "1.0.135", - "@socketsecurity/sdk": "1.4.16", + "@socketsecurity/sdk": "1.4.17", "blessed": "0.1.81", "blessed-contrib": "4.11.0", "browserslist": "4.24.4", From 4a0e7a9f3f8ba13d5b458785165ee287f52d8b0e Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 30 Mar 2025 11:40:58 -0400 Subject: [PATCH 2/3] Remove alert rules --- .config/rollup.test.config.mjs | 1 - src/utils/alert/artifact.ts | 5 +- src/utils/alert/rules.ts | 262 ---------------------- test/alert-rules.test.ts | 384 --------------------------------- 4 files changed, 1 insertion(+), 651 deletions(-) delete mode 100644 src/utils/alert/rules.ts delete mode 100644 test/alert-rules.test.ts diff --git a/.config/rollup.test.config.mjs b/.config/rollup.test.config.mjs index c29d9a4ef..5bf4339ea 100644 --- a/.config/rollup.test.config.mjs +++ b/.config/rollup.test.config.mjs @@ -18,7 +18,6 @@ export default () => { const { rootSrcPath } = constants return baseConfig({ input: { - 'alert-rules': `${rootSrcPath}/utils/alert/rules.ts`, errors: `${rootSrcPath}/utils/errors.ts`, 'path-resolve': `${rootSrcPath}/utils/path-resolve.ts` }, diff --git a/src/utils/alert/artifact.ts b/src/utils/alert/artifact.ts index 3d745f2ab..febaec8d9 100755 --- a/src/utils/alert/artifact.ts +++ b/src/utils/alert/artifact.ts @@ -29,10 +29,7 @@ export type ArtifactAlertUpgrade = Remap< export type CveAlertType = 'cve' | 'mediumCVE' | 'mildCVE' | 'criticalCVE' export type CompactSocketArtifactAlert = Remap< - Omit< - SocketArtifactAlert, - 'action' | 'actionPolicyIndex' | 'category' | 'end' | 'file' | 'start' - > + Omit > export type CompactSocketArtifact = Remap< diff --git a/src/utils/alert/rules.ts b/src/utils/alert/rules.ts deleted file mode 100644 index 85d6cb8bc..000000000 --- a/src/utils/alert/rules.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { isObject } from '@socketsecurity/registry/lib/objects' - -import { findSocketYmlSync, getConfigValue } from '../config' -import { isErrnoException } from '../errors' -import { getPublicToken, setupSdk } from '../sdk' - -import type { SocketSdkResultType } from '@socketsecurity/sdk' - -type AlertUxLookup = ReturnType - -type AlertUxLookupSettings = Parameters[0] - -type AlertUxLookupResult = ReturnType - -type NonNormalizedRule = - | NonNullable< - NonNullable< - NonNullable< - (SocketSdkResultType<'postSettings'> & { - success: true - })['data']['entries'][number]['settings'][string] - >['issueRules'] - > - >[string] - | boolean - -type NonNormalizedResolvedRule = - | (NonNullable< - NonNullable< - (SocketSdkResultType<'postSettings'> & { - success: true - })['data']['defaults']['issueRules'] - >[string] - > & { action: string }) - | boolean - -type RuleActionUX = { block: boolean; display: boolean } - -const ERROR_UX: RuleActionUX = { - block: true, - display: true -} - -const IGNORE_UX: RuleActionUX = { - block: false, - display: false -} - -const WARN_UX: RuleActionUX = { - block: false, - display: true -} - -// Iterates over all entries with ordered issue rule for deferral. Iterates over -// all issue rules and finds the first defined value that does not defer otherwise -// uses the defaultValue. Takes the value and converts into a UX workflow. -function resolveAlertRuleUX( - orderedRulesCollection: Iterable>, - defaultValue: NonNormalizedResolvedRule -): RuleActionUX { - if ( - defaultValue === true || - defaultValue === null || - defaultValue === undefined - ) { - defaultValue = { action: 'error' } - } else if (defaultValue === false) { - defaultValue = { action: 'ignore' } - } - let block = false - let display = false - let needDefault = true - iterate_entries: for (const rules of orderedRulesCollection) { - for (const rule of rules) { - if (ruleValueDoesNotDefer(rule)) { - needDefault = false - const narrowingFilter = uxForDefinedNonDeferValue(rule) - block = block || narrowingFilter.block - display = display || narrowingFilter.display - continue iterate_entries - } - } - const narrowingFilter = uxForDefinedNonDeferValue(defaultValue) - block = block || narrowingFilter.block - display = display || narrowingFilter.display - } - if (needDefault) { - const narrowingFilter = uxForDefinedNonDeferValue(defaultValue) - block = block || narrowingFilter.block - display = display || narrowingFilter.display - } - return { block, display } -} - -// Negative form because it is narrowing the type. -function ruleValueDoesNotDefer( - rule: NonNormalizedRule -): rule is NonNormalizedResolvedRule { - if (rule === undefined) { - return false - } - if (isObject(rule)) { - const { action } = rule - if (action === undefined || action === 'defer') { - return false - } - } - return true -} - -// Handles booleans for backwards compatibility. -function uxForDefinedNonDeferValue( - ruleValue: NonNormalizedResolvedRule -): RuleActionUX { - if (typeof ruleValue === 'boolean') { - return ruleValue ? ERROR_UX : IGNORE_UX - } - const { action } = ruleValue - if (action === 'warn') { - return WARN_UX - } else if (action === 'ignore') { - return IGNORE_UX - } - return ERROR_UX -} - -type SettingsType = (SocketSdkResultType<'postSettings'> & { - success: true -})['data'] - -export function createAlertUXLookup(settings: SettingsType): (context: { - package: { name: string; version: string } - alert: { type: string } -}) => RuleActionUX { - const cachedUX: Map = - new Map() - return context => { - const { type } = context.alert - let ux = cachedUX.get(type) - if (ux) { - return ux - } - const orderedRulesCollection: NonNormalizedRule[][] = [] - for (const settingsEntry of settings.entries) { - const orderedRules: NonNormalizedRule[] = [] - let target = settingsEntry.start - while (target !== null) { - const resolvedTarget = settingsEntry.settings[target] - if (!resolvedTarget) { - break - } - const issueRuleValue = resolvedTarget.issueRules?.[type] - if (typeof issueRuleValue !== 'undefined') { - orderedRules.push(issueRuleValue) - } - target = resolvedTarget.deferTo ?? null - } - orderedRulesCollection.push(orderedRules) - } - const defaultValue = settings.defaults.issueRules[type] as - | { action: 'error' | 'ignore' | 'warn' } - | boolean - | undefined - let resolvedDefaultValue: NonNormalizedResolvedRule = { - action: 'error' - } - if (defaultValue === false) { - resolvedDefaultValue = { action: 'ignore' } - } else if (defaultValue && defaultValue !== true) { - resolvedDefaultValue = { action: defaultValue.action ?? 'error' } - } - ux = resolveAlertRuleUX(orderedRulesCollection, resolvedDefaultValue) - cachedUX.set(type, ux) - return ux - } -} - -let _uxLookup: AlertUxLookup | undefined -export async function uxLookup( - settings: AlertUxLookupSettings -): Promise { - if (_uxLookup === undefined) { - const { orgs, settings } = await (async () => { - try { - const sockSdk = await setupSdk(getPublicToken()) - const orgResult = await sockSdk.getOrganizations() - if (!orgResult.success) { - if (orgResult.status === 429) { - throw new Error(`API token quota exceeded: ${orgResult.error}`) - } - throw new Error( - `Failed to fetch Socket organization info: ${orgResult.error}` - ) - } - const { organizations } = orgResult.data - const orgs: Array> = - [] - for (const org of Object.values(organizations)) { - if (org) { - orgs.push(org) - } - } - const settingsResult = await sockSdk.postSettings( - orgs.map(org => ({ organization: org.id })) - ) - if (!settingsResult.success) { - throw new Error( - `Failed to fetch API key settings: ${settingsResult.error}` - ) - } - return { - orgs, - settings: settingsResult.data - } - } catch (e) { - const cause = isObject(e) && 'cause' in e ? e['cause'] : undefined - if ( - isErrnoException(cause) && - (cause.code === 'ENOTFOUND' || cause.code === 'ECONNREFUSED') - ) { - throw new Error( - 'Unable to connect to socket.dev, ensure internet connectivity before retrying', - { - cause: e - } - ) - } - throw e - } - })() - // Remove any organizations not being enforced. - const enforcedOrgs = getConfigValue('enforcedOrgs') ?? [] - for (const { 0: i, 1: org } of orgs.entries()) { - if (!enforcedOrgs.includes(org.id)) { - settings.entries.splice(i, 1) - } - } - const socketYml = findSocketYmlSync() - if (socketYml) { - settings.entries.push({ - start: socketYml.path, - settings: { - [socketYml.path]: { - deferTo: null, - // TODO: TypeScript complains about the type not matching. We should - // figure out why are providing - // issueRules: { [issueName: string]: boolean } - // but expecting - // issueRules: { [issueName: string]: { action: 'defer' | 'error' | 'ignore' | 'monitor' | 'warn' } } - issueRules: socketYml.parsed.issueRules as unknown as { - [key: string]: { - action: 'defer' | 'error' | 'ignore' | 'monitor' | 'warn' - } - } - } - } - }) - } - _uxLookup = createAlertUXLookup(settings) - } - return _uxLookup(settings) -} diff --git a/test/alert-rules.test.ts b/test/alert-rules.test.ts deleted file mode 100644 index 32da0ba93..000000000 --- a/test/alert-rules.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { createAlertUXLookup } from './dist/alert-rules' - -describe('Alert Rule UX', () => { - it('should properly defer', () => { - const noEntriesLookup = createAlertUXLookup({ - defaults: { - issueRules: { - fromDeferString: { - action: 'warn' - }, - fromUndefinedAction: { - action: 'warn' - }, - fromUndefinedRule: { - action: 'warn' - }, - willError: { - action: 'error' - }, - willIgnore: { - action: 'ignore' - }, - willWarn: { - action: 'warn' - } - } - }, - entries: [ - { - start: 'organization', - settings: { - organization: { - deferTo: 'repository', - issueRules: { - fromDeferString: { action: 'defer' }, - // @ts-ignore paranoia - fromUndefinedAction: {} - } - }, - repository: { - deferTo: null, - issueRules: { - fromMiddleConfig: { - action: 'warn' - } - } - } - } - } - ] - }) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'willError' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'willIgnore' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": false, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'willWarn' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": true, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'fromDeferString' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": true, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'fromUndefinedAction' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": true, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'fromUndefinedRule' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": true, - } - `) - expect( - noEntriesLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'fromMiddleConfig' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": true, - } - `) - }) - it('should use error UX when missing keys', () => { - const emptyLookup = createAlertUXLookup({ - defaults: { - issueRules: {} - }, - entries: [] - }) - expect( - emptyLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: '404' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - }) - it('should use error/ignore UX when having boolean values instead of config', () => { - const booleanLookup = createAlertUXLookup({ - defaults: { - issueRules: { - // @ts-ignore backcompat - defaultTrue: true, - // @ts-ignore backcompat - defaultFalse: false - } - }, - entries: [ - { - start: 'organization', - settings: { - organization: { - issueRules: { - // @ts-ignore backcompat - orgTrue: true, - // @ts-ignore backcompat - orgFalse: false - } - } - } - } - ] - }) - expect( - booleanLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'defaultTrue' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - expect( - booleanLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'orgTrue' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - expect( - booleanLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'defaultFalse' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": false, - } - `) - expect( - booleanLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'orgFalse' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": false, - } - `) - }) - it('should use the maximal strength on multiple settings entries', () => { - const multiSettings = createAlertUXLookup({ - defaults: { - issueRules: {} - }, - entries: [ - { - start: 'start', - settings: { - start: { - deferTo: null, - issueRules: { - warn_then_error: { - action: 'warn' - }, - ignore_then_missing: { - action: 'ignore' - }, - ignore_then_defer: { - action: 'ignore' - } - } - } - } - }, - { - start: 'start', - settings: { - start: { - deferTo: null, - issueRules: { - warn_then_error: { - action: 'error' - }, - ignore_then_defer: { - action: 'defer' - } - } - } - } - } - ] - }) - expect( - multiSettings({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'warn_then_error' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - expect( - multiSettings({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'ignore_then_missing' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - expect( - multiSettings({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'ignore_then_defer' } - }) - ).toMatchInlineSnapshot(` - { - "block": true, - "display": true, - } - `) - }) - it('should shadow defaults', () => { - const shadowedLookup = createAlertUXLookup({ - defaults: { - issueRules: { - willWarn: { - action: 'warn' - } - } - }, - entries: [ - { - start: 'organization', - settings: { - organization: { - deferTo: null, - issueRules: { - willWarn: { - action: 'ignore' - } - } - } - } - } - ] - }) - expect( - shadowedLookup({ - package: { - name: 'bar', - version: '0.0.0' - }, - alert: { type: 'willWarn' } - }) - ).toMatchInlineSnapshot(` - { - "block": false, - "display": false, - } - `) - }) -}) From 64b9c442e65f86c8d9cf48477c72e240afd74440 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 30 Mar 2025 18:28:26 -0400 Subject: [PATCH 3/3] Make safe npm alerts prettier --- src/constants.ts | 8 +- src/shadow/npm/arborist/lib/arborist/index.ts | 15 +- src/utils/alert/artifact.ts | 24 ++- src/utils/config.ts | 3 +- src/utils/socket-package-alert.ts | 144 ++++++++++++------ 5 files changed, 137 insertions(+), 57 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index c8a14f812..095ff3035 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -47,6 +47,7 @@ type ENV = Remap< SOCKET_CLI_ACCEPT_RISKS: boolean SOCKET_CLI_DEBUG: boolean SOCKET_CLI_NO_API_TOKEN: boolean + SOCKET_CLI_VIEW_ALL_RISKS: boolean SOCKET_SECURITY_API_BASE_URL: string SOCKET_SECURITY_API_TOKEN: string XDG_DATA_HOME: string @@ -110,6 +111,7 @@ type Constants = Remap< readonly SOCKET_CLI_SENTRY_NPM_BIN_NAME: 'socket-npm-with-sentry' readonly SOCKET_CLI_SENTRY_NPX_BIN_NAME: 'socket-npx-with-sentry' readonly SOCKET_CLI_SENTRY_PACKAGE_NAME: '@socketsecurity/cli-with-sentry' + readonly SOCKET_CLI_VIEW_ALL_RISKS: 'SOCKET_CLI_VIEW_ALL_RISKS' readonly SOCKET_SECURITY_API_BASE_URL: 'SOCKET_SECURITY_API_BASE_URL' readonly SOCKET_SECURITY_API_TOKEN: 'SOCKET_SECURITY_API_TOKEN' readonly VLT: 'vlt' @@ -183,6 +185,7 @@ const SOCKET_CLI_SENTRY_BIN_NAME_ALIAS = 'cli-with-sentry' const SOCKET_CLI_SENTRY_NPM_BIN_NAME = 'socket-npm-with-sentry' const SOCKET_CLI_SENTRY_NPX_BIN_NAME = 'socket-npx-with-sentry' const SOCKET_CLI_SENTRY_PACKAGE_NAME = `${SOCKET_SECURITY_SCOPE}/cli-with-sentry` +const SOCKET_CLI_VIEW_ALL_RISKS = 'SOCKET_CLI_VIEW_ALL_RISKS' const SOCKET_SECURITY_API_BASE_URL = 'SOCKET_SECURITY_API_BASE_URL' const SOCKET_SECURITY_API_TOKEN = 'SOCKET_SECURITY_API_TOKEN' const VLT = 'vlt' @@ -221,12 +224,14 @@ const LAZY_ENV = () => { // non-roaming application data, like temporary files, cached data, and program // settings, that are specific to the current machine and user. LOCALAPPDATA: envAsString(env['LOCALAPPDATA']), - // Flag to accepts risks of previous safe npm/npx run. + // Flag to accepts risks of safe-npm and safe-npx run. SOCKET_CLI_ACCEPT_RISKS: envAsBoolean(env['SOCKET_CLI_ACCEPT_RISKS']), // Flag to help debug Socket CLI. SOCKET_CLI_DEBUG: envAsBoolean(env['SOCKET_CLI_DEBUG']), // Flag to make the default API token `undefined`. SOCKET_CLI_NO_API_TOKEN: envAsBoolean(env['SOCKET_CLI_NO_API_TOKEN']), + // Flag to view all risks of safe-npm and safe-npx run. + SOCKET_CLI_VIEW_ALL_RISKS: envAsBoolean(env['SOCKET_CLI_VIEW_ALL_RISKS']), // Flag to change the base URL for all API-calls. // https://github.com/SocketDev/socket-cli?tab=readme-ov-file#environment-variables-for-development SOCKET_SECURITY_API_BASE_URL: envAsString( @@ -386,6 +391,7 @@ const constants = createConstantsObject( SOCKET_CLI_SENTRY_NPM_BIN_NAME, SOCKET_CLI_SENTRY_NPX_BIN_NAME, SOCKET_CLI_SENTRY_PACKAGE_NAME, + SOCKET_CLI_VIEW_ALL_RISKS, SOCKET_SECURITY_API_BASE_URL, SOCKET_SECURITY_API_TOKEN, VLT, diff --git a/src/shadow/npm/arborist/lib/arborist/index.ts b/src/shadow/npm/arborist/lib/arborist/index.ts index 57d655416..0235f021d 100755 --- a/src/shadow/npm/arborist/lib/arborist/index.ts +++ b/src/shadow/npm/arborist/lib/arborist/index.ts @@ -1,5 +1,7 @@ import process from 'node:process' +import { stripIndents } from 'common-tags' + import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../../../../constants' @@ -15,6 +17,7 @@ const { NPX, SOCKET_CLI_ACCEPT_RISKS, SOCKET_CLI_SAFE_WRAPPER, + SOCKET_CLI_VIEW_ALL_RISKS, kInternalsSymbol, [kInternalsSymbol as unknown as 'Symbol(kInternalsSymbol)']: { getIpc } } = constants @@ -121,9 +124,17 @@ export class SafeArborist extends Arborist { }) if (alertsMap.size) { process.exitCode = 1 - logAlertsMap(alertsMap, { output: process.stderr }) + logAlertsMap(alertsMap, { + // Lazily access constants.ENV[SOCKET_CLI_VIEW_ALL_RISKS]. + hideAt: constants.ENV[SOCKET_CLI_VIEW_ALL_RISKS] ? 'none' : 'middle', + output: process.stderr + }) throw new Error( - `Socket ${binName} exiting due to risks.\nRerun with environment variable ${SOCKET_CLI_ACCEPT_RISKS}=1 to accept risks.` + stripIndents` + Socket ${binName} exiting due to risks. + To view all risks rerun with environment variable ${SOCKET_CLI_VIEW_ALL_RISKS}=1. + To accept risks rerun with environment variable ${SOCKET_CLI_ACCEPT_RISKS}=1. + ` ) } else { logger.success(`Socket ${binName} found no risks!`) diff --git a/src/utils/alert/artifact.ts b/src/utils/alert/artifact.ts index febaec8d9..9158037d0 100755 --- a/src/utils/alert/artifact.ts +++ b/src/utils/alert/artifact.ts @@ -1,17 +1,23 @@ import constants from '../../constants' import type { Remap } from '@socketsecurity/registry/lib/objects' -import type { components } from '@socketsecurity/sdk/types/api' +import type { components, operations } from '@socketsecurity/sdk/types/api' + +export type ALERT_TYPE = keyof NonNullable< + operations['getOrgSecurityPolicy']['responses']['200']['content']['application/json']['securityPolicyRules'] +> + +export type CVE_ALERT_TYPE = 'cve' | 'mediumCVE' | 'mildCVE' | 'criticalCVE' export type ArtifactAlertCve = Remap< Omit & { - type: CveAlertType + type: CVE_ALERT_TYPE } > export type ArtifactAlertCveFixable = Remap< Omit & { - type: CveAlertType + type: CVE_ALERT_TYPE props: { firstPatchedVersionIdentifier: string vulnerableVersionRange: string @@ -26,8 +32,6 @@ export type ArtifactAlertUpgrade = Remap< } > -export type CveAlertType = 'cve' | 'mediumCVE' | 'mildCVE' | 'criticalCVE' - export type CompactSocketArtifactAlert = Remap< Omit > @@ -38,10 +42,16 @@ export type CompactSocketArtifact = Remap< } > -export type SocketArtifact = components['schemas']['SocketArtifact'] +export type SocketArtifact = Remap< + Omit & { + alerts?: SocketArtifactAlert[] + } +> export type SocketArtifactAlert = Remap< - Omit & { + Omit & { + type: ALERT_TYPE + action?: 'error' | 'monitor' | 'warn' | 'ignore' props?: any | undefined } > diff --git a/src/utils/config.ts b/src/utils/config.ts index 23f755c3f..3bedb84be 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -136,9 +136,8 @@ function normalizeConfigKey(key: keyof LocalConfig): keyof LocalConfig { return normalizedKey } -export function findSocketYmlSync() { +export function findSocketYmlSync(dir = process.cwd()) { let prevDir = null - let dir = process.cwd() while (dir !== prevDir) { let ymlPath = path.join(dir, 'socket.yml') let yml = safeReadFileSync(ymlPath) diff --git a/src/utils/socket-package-alert.ts b/src/utils/socket-package-alert.ts index 786980d22..5a7084c19 100644 --- a/src/utils/socket-package-alert.ts +++ b/src/utils/socket-package-alert.ts @@ -7,19 +7,20 @@ import { hasOwn } from '@socketsecurity/registry/lib/objects' import { resolvePackageName } from '@socketsecurity/registry/lib/packages' import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { - CompactSocketArtifact, - CompactSocketArtifactAlert, - isArtifactAlertCve -} from './alert/artifact' +import { isArtifactAlertCve } from './alert/artifact' import { ALERT_FIX_TYPE } from './alert/fix' -import { uxLookup } from './alert/rules' import { ALERT_SEVERITY } from './alert/severity' import { ColorOrMarkdown } from './color-or-markdown' import { getSocketDevPackageOverviewUrl } from './socket-url' import { getTranslations } from './translations' import constants from '../constants' +import { findSocketYmlSync } from './config' +import type { + ALERT_TYPE, + CompactSocketArtifact, + CompactSocketArtifactAlert +} from './alert/artifact' import type { Spinner } from '@socketsecurity/registry/lib/spinner' export enum ALERT_SEVERITY_COLOR { @@ -29,7 +30,7 @@ export enum ALERT_SEVERITY_COLOR { low = 'white' } -export const enum ALERT_SEVERITY_ORDER { +export enum ALERT_SEVERITY_ORDER { critical = 0, high = 1, middle = 2, @@ -44,7 +45,6 @@ export type SocketPackageAlert = { type: string blocked: boolean critical: boolean - display: boolean fixable: boolean raw: CompactSocketArtifactAlert upgradable: boolean @@ -56,6 +56,10 @@ const { CVE_ALERT_PROPS_FIRST_PATCHED_VERSION_IDENTIFIER, NPM } = constants const format = new ColorOrMarkdown(false) +function alertsHaveBlocked(alerts: SocketPackageAlert[]): boolean { + return alerts.find(a => a.blocked) !== undefined +} + function alertsHaveSeverity( alerts: SocketPackageAlert[], severity: `${ALERT_SEVERITY}` @@ -84,7 +88,8 @@ function getAlertSeverityOrder(alert: SocketPackageAlert): number { } function getAlertsSeverityOrder(alerts: SocketPackageAlert[]): number { - return alertsHaveSeverity(alerts, ALERT_SEVERITY.critical) + return alertsHaveBlocked(alerts) || + alertsHaveSeverity(alerts, ALERT_SEVERITY.critical) ? 0 : alertsHaveSeverity(alerts, ALERT_SEVERITY.high) ? 1 @@ -96,29 +101,46 @@ function getAlertsSeverityOrder(alerts: SocketPackageAlert[]): number { } export type RiskCounts = { + critical: number + high: number middle: number low: number } function getHiddenRiskCounts(hiddenAlerts: SocketPackageAlert[]): RiskCounts { - let hiddenMidCount = 0 - let hiddenLowCount = 0 + const riskCounts = { + critical: 0, + high: 0, + middle: 0, + low: 0 + } for (const alert of hiddenAlerts) { - const order = getAlertSeverityOrder(alert) - if (order === ALERT_SEVERITY_ORDER.middle) { - hiddenMidCount += 1 - } else if (order === ALERT_SEVERITY_ORDER.low) { - hiddenLowCount += 1 + switch (getAlertSeverityOrder(alert)) { + case ALERT_SEVERITY_ORDER.critical: + riskCounts.critical += 1 + break + case ALERT_SEVERITY_ORDER.high: + riskCounts.high += 1 + break + case ALERT_SEVERITY_ORDER.middle: + riskCounts.middle += 1 + break + case ALERT_SEVERITY_ORDER.low: + riskCounts.low += 1 + break } } - return { - middle: hiddenMidCount, - low: hiddenLowCount - } + return riskCounts } function getHiddenRisksDescription(riskCounts: RiskCounts): string { const descriptions: string[] = [] + if (riskCounts.critical) { + descriptions.push(`${riskCounts.critical} ${getSeverityLabel('critical')}`) + } + if (riskCounts.high) { + descriptions.push(`${riskCounts.high} ${getSeverityLabel('high')}`) + } if (riskCounts.middle) { descriptions.push(`${riskCounts.middle} ${getSeverityLabel('middle')}`) } @@ -181,14 +203,22 @@ export async function addArtifactToAlertsMap( const { version } = artifact const pkgId = `${name}@${version}` const major = semver.major(version) + const socketYml = findSocketYmlSync() + const enabledState = { + __proto__: null, + ...socketYml?.parsed.issueRules + } as Partial> let sockPkgAlerts: SocketPackageAlert[] = [] for (const alert of artifact.alerts) { - // eslint-disable-next-line no-await-in-loop - const ux = await uxLookup({ - package: { name, version }, - alert: { type: alert.type } - }) - const blocked = ux.block + const action = alert.action ?? '' + const enabledFlag = enabledState[alert.type] + if ( + (action === 'ignore' && enabledFlag !== true) || + enabledFlag === false + ) { + continue + } + const blocked = action === 'error' const critical = alert.severity === ALERT_SEVERITY.critical const cve = isArtifactAlertCve(alert) const fixType = alert.fix?.type ?? '' @@ -210,7 +240,6 @@ export async function addArtifactToAlertsMap( type: alert.type, blocked, critical, - display: ux.display, fixable, raw: alert, upgradable @@ -326,6 +355,7 @@ export function getCveInfoByAlertsMap( } export type LogAlertsMapOptions = { + hideAt?: `${ALERT_SEVERITY}` | 'none' | undefined output?: NodeJS.WriteStream | undefined } @@ -333,7 +363,7 @@ export function logAlertsMap( alertsMap: AlertsByPkgId, options: LogAlertsMapOptions ) { - const { output = process.stderr } = { + const { hideAt = 'middle', output = process.stderr } = { __proto__: null, ...options } as LogAlertsMapOptions @@ -342,11 +372,16 @@ export function logAlertsMap( (a, b) => getAlertsSeverityOrder(a[1]) - getAlertsSeverityOrder(b[1]) ) const hiddenEntries: typeof sortedEntries = [] - for (let i = 0, { length } = sortedEntries; i < length; i += 1) { + for ( + let i = 0, prevDimmed = false, { length } = sortedEntries; + i < length; + i += 1 + ) { const { 0: pkgId, 1: alerts } = sortedEntries[i]! const hiddenAlerts: typeof alerts = [] const filteredAlerts = alerts.filter(a => { - const keep = getAlertSeverityOrder(a) <= ALERT_SEVERITY_ORDER.high + const keep = + a.blocked || getAlertSeverityOrder(a) < ALERT_SEVERITY_ORDER[hideAt] if (!keep) { hiddenAlerts.push(a) } @@ -358,13 +393,18 @@ export function logAlertsMap( if (!filteredAlerts.length) { continue } - const lines = new Set() + const lines = new Set() const sortedAlerts = filteredAlerts.sort(alertSeverityComparator) + const isDimmed = !sortedAlerts.find( + a => a.blocked || getAlertSeverityOrder(a) < ALERT_SEVERITY_ORDER.middle + ) for (const alert of sortedAlerts) { const { type } = alert const severity = alert.raw.severity ?? '' const attributes = [ - ...(severity ? [colors[ALERT_SEVERITY_COLOR[severity]](severity)] : []), + ...(severity + ? [colors[ALERT_SEVERITY_COLOR[severity]](getSeverityLabel(severity))] + : []), ...(alert.blocked ? [colors.bold(colors.red('blocked'))] : []), ...(alert.fixable ? ['fixable'] : []) ] @@ -376,21 +416,29 @@ export function logAlertsMap( const info = (translations.alerts as any)[type] const title = info?.title ?? type const maybeDesc = info?.description ? ` - ${info.description}` : '' + const content = `${title}${maybeAttributes}${maybeDesc}` // TODO: emoji seems to mis-align terminals sometimes - lines.add(` ${title}${maybeAttributes}${maybeDesc}`) + if (isDimmed) { + lines.add(` ${colors.dim(content)}`) + } else { + lines.add(` ${content}`) + } } const purlObj = PackageURL.fromString(`pkg:npm/${pkgId}`) - output.write( - `${i ? '\n' : ''}${format.hyperlink( - pkgId, - getSocketDevPackageOverviewUrl( - NPM, - resolvePackageName(purlObj), - purlObj.version - ) - )}:\n` + const hyperlink = format.hyperlink( + pkgId, + getSocketDevPackageOverviewUrl( + NPM, + resolvePackageName(purlObj), + purlObj.version + ) ) + if (isDimmed) { + output.write(`${prevDimmed ? '' : '\n'}${colors.dim(`${hyperlink}:`)}\n`) + } else { + output.write(`${i ? '\n' : ''}${hyperlink}:\n`) + } for (const line of lines) { output.write(`${line}\n`) } @@ -398,28 +446,34 @@ export function logAlertsMap( if (hiddenAlertsCount) { if (hiddenAlertsCount === 1) { output.write( - ` +1 Hidden ${getSeverityLabel(hiddenAlerts[0]!.raw.severity ?? 'low')} risk alert\n` + ` ${colors.dim(`+1 Hidden ${getSeverityLabel(hiddenAlerts[0]!.raw.severity ?? 'low')} risk alert`)}\n` ) } else { output.write( - ` +${hiddenAlertsCount} Hidden alerts ${colors.italic(getHiddenRisksDescription(getHiddenRiskCounts(hiddenAlerts)))}\n` + ` ${colors.dim(`+${hiddenAlertsCount} Hidden alerts ${colors.italic(getHiddenRisksDescription(getHiddenRiskCounts(hiddenAlerts)))}`)}\n` ) } } + prevDimmed = isDimmed } const { length: hiddenEntriesCount } = hiddenEntries if (hiddenEntriesCount) { const totalRiskCounts = { + critical: 0, + high: 0, middle: 0, low: 0 } for (const { 1: alerts } of hiddenEntries) { const riskCounts = getHiddenRiskCounts(alerts) + totalRiskCounts.critical += riskCounts.critical + totalRiskCounts.high += riskCounts.high totalRiskCounts.middle += riskCounts.middle totalRiskCounts.low += riskCounts.low } output.write( - `\n+${hiddenEntriesCount} Packages with hidden alerts ${colors.italic(getHiddenRisksDescription(totalRiskCounts))}\n\n` + `\n${colors.dim(`+${hiddenEntriesCount} Packages with hidden alerts ${colors.italic(getHiddenRisksDescription(totalRiskCounts))}`)}\n` ) } + output.write('\n') }