@underpostnet/cyberia 3.3.76 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/.github/workflows/coverall.cyberia.ci.yml +17 -3
- package/.github/workflows/cyberia-client.cd.yml +2 -1
- package/.github/workflows/cyberia-server.cd.yml +2 -1
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +7 -1
- package/.github/workflows/ghpkg.ci.yml +19 -2
- package/.github/workflows/gitlab.ci.yml +8 -6
- package/.github/workflows/hardhat.ci.yml +4 -1
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/.github/workflows/publish.ci.yml +21 -12
- package/.github/workflows/publish.cyberia.ci.yml +25 -14
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -2
- package/CHANGELOG.md +110 -262
- package/CLI-HELP.md +29 -2
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/README.md +1 -1
- package/bin/build.js +67 -14
- package/bin/cyberia.js +4 -4
- package/bin/index.js +4 -4
- package/compose.env +1 -0
- package/conf.js +1 -1043
- package/deploy/cyberia-client/deploy.sh +13 -11
- package/deploy/cyberia-server/deploy.sh +13 -11
- package/deploy/dd-cyberia/sync-deploy.sh +7 -28
- package/deploy/lib/config.sh +1 -1
- package/deploy/lib/github-actions-logging.sh +3 -2
- package/deploy/lib/host.sh +1 -1
- package/deploy/pwa-microservices-template/deploy.sh +7 -1
- package/deployment.yaml +1 -1
- package/docker-compose.yml +7 -7
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +6 -6
- package/hardhat/package.json +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +19 -17
- package/scripts/test-monitor.sh +1 -1
- package/src/api/file/file.router.js +7 -1
- package/src/cli/baremetal.js +1 -1
- package/src/cli/cluster.js +73 -18
- package/src/cli/deploy.js +211 -30
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/index.js +36 -0
- package/src/cli/kubectl.js +4 -3
- package/src/cli/release.js +5 -1
- package/src/cli/repository.js +14 -11
- package/src/cli/run.js +10 -3
- package/src/cli/secrets.js +393 -200
- package/src/cli/socketsecurity.js +260 -0
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +11 -1
- package/src/client/components/core/Modal.js +189 -66
- package/src/client/components/core/SearchBox.js +0 -1
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +12 -0
- package/src/client/components/underpost/CssUnderpost.js +15 -2
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +8 -0
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +54 -56
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +30 -29
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +54 -25
- package/src/client/ssr/head/Css.js +12 -10
- package/src/client/ssr/views/Cyberia404.js +9 -13
- package/src/client/ssr/views/NotFound.js +75 -0
- package/src/client-builder/client-bundle.js +2 -2
- package/src/index.js +13 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/compose.env +1 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
- package/src/server/build/catalog.js +1 -0
- package/src/server/build/execution.js +19 -5
- package/src/server/build/package.js +166 -3
- package/src/server/build/testing.js +2 -1
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/runtime/conf.js +57 -7
- package/src/server/runtime/config-scope.js +2 -0
- package/src/server/runtime/process.js +4 -3
- package/src/server/runtime/start.js +11 -2
- package/src/server/security/socketsecurity.js +1735 -0
- package/test/integration/infra/1-security/config-scope.test.js +3 -0
- package/test/integration/infra/1-security/socketsecurity.test.js +1041 -0
- package/test/integration/infra/1-security/sops-secret-store.test.js +201 -71
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/build-template.test.js +24 -0
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/conf-resolution.test.js +41 -7
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +38 -0
- package/test/unit/execution-profiles.test.js +1 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/package.test.js +118 -0
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
|
@@ -0,0 +1,1735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket.dev security audit: dependency scans, reachability analysis, security patches, source
|
|
3
|
+
* code alerts on this project's own packages, and the report built over them and `npm audit`.
|
|
4
|
+
*
|
|
5
|
+
* The Socket CLI (`@socketsecurity/cli`) executes; this module resolves its configuration,
|
|
6
|
+
* builds its command lines, parses its JSON, and renders the report. The patch manifest
|
|
7
|
+
* (`.socket/manifest.json`) records the patches a checkout carries, and `socket patch apply`
|
|
8
|
+
* replays it on every install of the checkout through the `prepare` hook this module installs.
|
|
9
|
+
*
|
|
10
|
+
* @module src/server/security/socketsecurity.js
|
|
11
|
+
* @namespace SocketSecurityService
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
import fs from 'fs-extra';
|
|
16
|
+
import nodePath from 'node:path';
|
|
17
|
+
import { loggerFactory } from '../ops/logger.js';
|
|
18
|
+
import { environmentValueFactory } from '../runtime/environment.js';
|
|
19
|
+
import { shellArgumentFactory, shellExec } from '../runtime/process.js';
|
|
20
|
+
|
|
21
|
+
const logger = loggerFactory(import.meta);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Socket CLI integration.
|
|
25
|
+
* @class SocketSecurityService
|
|
26
|
+
* @memberof SocketSecurityService
|
|
27
|
+
*/
|
|
28
|
+
class SocketSecurityService {
|
|
29
|
+
/** Environment variables the Socket CLI reads; resolved through the platform env chain. */
|
|
30
|
+
static ENV = {
|
|
31
|
+
token: 'SOCKET_CLI_API_TOKEN',
|
|
32
|
+
orgSlug: 'SOCKET_CLI_ORG_SLUG',
|
|
33
|
+
acceptRisks: 'SOCKET_CLI_ACCEPT_RISKS',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
static CLI_PACKAGE = '@socketsecurity/cli';
|
|
37
|
+
static ECOSYSTEM = 'npm';
|
|
38
|
+
static MANIFEST_PATH = '.socket/manifest.json';
|
|
39
|
+
/** Reachability report `socket scan create --reach` writes next to the manifests it scans. */
|
|
40
|
+
static FACTS_PATH = '.socket.facts.json';
|
|
41
|
+
static DASHBOARD_URL = 'https://socket.dev/dashboard/org';
|
|
42
|
+
static REPORT_DIRECTORY = 'security-reports';
|
|
43
|
+
static REPORT_FILES = {
|
|
44
|
+
report: 'security-report.json',
|
|
45
|
+
markdown: 'security-report.md',
|
|
46
|
+
npmAudit: 'npm-audit.json',
|
|
47
|
+
scan: 'socket-scan.json',
|
|
48
|
+
reach: 'socket-reach.json',
|
|
49
|
+
scanArtifacts: 'socket-scan-artifacts.json',
|
|
50
|
+
packageAlerts: 'socket-package-alerts.json',
|
|
51
|
+
patchScan: 'socket-patch-scan.json',
|
|
52
|
+
/** Written by `socket fix --output-file`: the upgrades it computed. */
|
|
53
|
+
fixUpgrades: 'socket-fix-upgrades.json',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The apply command the `prepare` hook runs. */
|
|
57
|
+
static PATCH_APPLY_ARGS = ['patch', 'apply', '--ecosystems', SocketSecurityService.ECOSYSTEM];
|
|
58
|
+
/**
|
|
59
|
+
* The hook runs through the CLI, so an install on a host without Socket skips it. It is a
|
|
60
|
+
* `prepare` script: npm runs it on an install of this checkout, never on an install of the
|
|
61
|
+
* published tarball, so no consumer of the package executes it.
|
|
62
|
+
*/
|
|
63
|
+
static PREPARE_SCRIPT = 'node bin socketsecurity --patch-apply';
|
|
64
|
+
|
|
65
|
+
static SEVERITIES = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lowest policy level `socket scan create --report` reports. The CLI defaults to `error`, which
|
|
69
|
+
* drops every monitor, warn and ignore alert from the policy report the findings read their
|
|
70
|
+
* action from.
|
|
71
|
+
*/
|
|
72
|
+
static REPORT_LEVEL = 'defer';
|
|
73
|
+
|
|
74
|
+
/** Report domains: what an alert is about decides who fixes it. */
|
|
75
|
+
static DOMAINS = {
|
|
76
|
+
dependency:
|
|
77
|
+
'Risks in the external packages this checkout installs. An upgrade, a patch or a replacement fixes them.',
|
|
78
|
+
source:
|
|
79
|
+
"Alerts Socket raises on this project's own published package. A change to the code at the reported location fixes them.",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Socket alert severities on the report scale. */
|
|
83
|
+
static SOCKET_SEVERITIES = { low: 'low', middle: 'moderate', high: 'high', critical: 'critical' };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Alert categories by Socket alert type, in report order. `task` is the refactor a source
|
|
87
|
+
* finding in the category becomes.
|
|
88
|
+
*/
|
|
89
|
+
static ALERT_CATEGORIES = {
|
|
90
|
+
malware: {
|
|
91
|
+
label: 'Malware and supply chain attacks',
|
|
92
|
+
types: [
|
|
93
|
+
'malware',
|
|
94
|
+
'gptMalware',
|
|
95
|
+
'didYouMean',
|
|
96
|
+
'gptDidYouMean',
|
|
97
|
+
'troll',
|
|
98
|
+
'compromisedSSHKey',
|
|
99
|
+
'manifestConfusion',
|
|
100
|
+
'shellScriptOverride',
|
|
101
|
+
'binScriptConfusion',
|
|
102
|
+
'suspiciousStarActivity',
|
|
103
|
+
'telemetry',
|
|
104
|
+
],
|
|
105
|
+
task: 'Treat as an incident: confirm the code path, remove it, and rotate every credential it can reach.',
|
|
106
|
+
},
|
|
107
|
+
advisories: {
|
|
108
|
+
label: 'Registry advisories',
|
|
109
|
+
types: ['criticalCVE', 'cve', 'mediumCVE', 'mildCVE'],
|
|
110
|
+
task: 'Fix the vulnerable code path and publish a patched version.',
|
|
111
|
+
},
|
|
112
|
+
installScripts: {
|
|
113
|
+
label: 'Install scripts and native code',
|
|
114
|
+
types: ['installScripts', 'hasNativeCode'],
|
|
115
|
+
task: 'Remove the lifecycle script or the binary, or document why the install needs it.',
|
|
116
|
+
},
|
|
117
|
+
reputation: {
|
|
118
|
+
label: 'Package reputation and maintenance',
|
|
119
|
+
types: [
|
|
120
|
+
'unpopularPackage',
|
|
121
|
+
'unmaintained',
|
|
122
|
+
'deprecated',
|
|
123
|
+
'newAuthor',
|
|
124
|
+
'unstableOwnership',
|
|
125
|
+
'missingAuthor',
|
|
126
|
+
'noAuthorData',
|
|
127
|
+
'trivialPackage',
|
|
128
|
+
'emptyPackage',
|
|
129
|
+
'majorRefactor',
|
|
130
|
+
'chronoAnomaly',
|
|
131
|
+
'semverAnomaly',
|
|
132
|
+
'unpublished',
|
|
133
|
+
'missingTarball',
|
|
134
|
+
'noRepository',
|
|
135
|
+
'noV1',
|
|
136
|
+
'noTests',
|
|
137
|
+
'noREADME',
|
|
138
|
+
'noBugTracker',
|
|
139
|
+
'noWebsite',
|
|
140
|
+
'badSemver',
|
|
141
|
+
],
|
|
142
|
+
task: 'Correct the package metadata the alert names.',
|
|
143
|
+
},
|
|
144
|
+
resolution: {
|
|
145
|
+
label: 'Dependency resolution',
|
|
146
|
+
types: [
|
|
147
|
+
'gitDependency',
|
|
148
|
+
'gitHubDependency',
|
|
149
|
+
'httpDependency',
|
|
150
|
+
'fileDependency',
|
|
151
|
+
'floatingDependency',
|
|
152
|
+
'missingDependency',
|
|
153
|
+
'extraneousDependency',
|
|
154
|
+
'uncaughtOptionalDependency',
|
|
155
|
+
'unusedDependency',
|
|
156
|
+
'unresolvedRequire',
|
|
157
|
+
'peerDependency',
|
|
158
|
+
'shrinkwrap',
|
|
159
|
+
'badSemverDependency',
|
|
160
|
+
'invalidPackageJSON',
|
|
161
|
+
'typeModuleCompatibility',
|
|
162
|
+
'socketUpgradeAvailable',
|
|
163
|
+
],
|
|
164
|
+
task: 'Declare the dependency in package.json with a registry version range, or remove the import.',
|
|
165
|
+
},
|
|
166
|
+
license: {
|
|
167
|
+
label: 'License',
|
|
168
|
+
types: [
|
|
169
|
+
'licenseSpdxDisj',
|
|
170
|
+
'copyleftLicense',
|
|
171
|
+
'nonpermissiveLicense',
|
|
172
|
+
'unidentifiedLicense',
|
|
173
|
+
'noLicenseFound',
|
|
174
|
+
'explicitlyUnlicensedItem',
|
|
175
|
+
'miscLicenseIssues',
|
|
176
|
+
'deprecatedLicense',
|
|
177
|
+
'deprecatedException',
|
|
178
|
+
'licenseChange',
|
|
179
|
+
'licenseException',
|
|
180
|
+
'missingLicense',
|
|
181
|
+
'mixedLicense',
|
|
182
|
+
'ambiguousClassifier',
|
|
183
|
+
'modifiedException',
|
|
184
|
+
'modifiedLicense',
|
|
185
|
+
'nonFSFLicense',
|
|
186
|
+
'nonOSILicense',
|
|
187
|
+
'nonSPDXLicense',
|
|
188
|
+
'notice',
|
|
189
|
+
'unclearLicense',
|
|
190
|
+
'unsafeCopyright',
|
|
191
|
+
],
|
|
192
|
+
task: 'Record the license of the flagged file, or replace the file.',
|
|
193
|
+
},
|
|
194
|
+
codeAnomaly: {
|
|
195
|
+
label: 'Code anomalies',
|
|
196
|
+
types: [
|
|
197
|
+
'gptAnomaly',
|
|
198
|
+
'gptSecurity',
|
|
199
|
+
'potentialVulnerability',
|
|
200
|
+
'obfuscatedFile',
|
|
201
|
+
'obfuscatedRequire',
|
|
202
|
+
'highEntropyStrings',
|
|
203
|
+
'suspiciousString',
|
|
204
|
+
'longStrings',
|
|
205
|
+
'minifiedFile',
|
|
206
|
+
'bidi',
|
|
207
|
+
'invisibleChars',
|
|
208
|
+
'zeroWidth',
|
|
209
|
+
'homoglyphs',
|
|
210
|
+
'badEncoding',
|
|
211
|
+
],
|
|
212
|
+
task: 'Review the flagged code. Fix and test a real defect; keep generated or minified content out of the source tree.',
|
|
213
|
+
},
|
|
214
|
+
debugAccess: {
|
|
215
|
+
label: 'Debug, reflection and dynamic code',
|
|
216
|
+
types: ['debugAccess', 'usesEval', 'dynamicRequire'],
|
|
217
|
+
task: 'Replace eval, new Function and computed imports with static imports or an explicit allow-list.',
|
|
218
|
+
},
|
|
219
|
+
envVars: {
|
|
220
|
+
label: 'Environment variable access',
|
|
221
|
+
types: ['envVars'],
|
|
222
|
+
task: 'Read the key through environmentValueFactory and declare its owner in CONFIG_OWNERSHIP (src/server/runtime/config-scope.js).',
|
|
223
|
+
},
|
|
224
|
+
filesystem: {
|
|
225
|
+
label: 'Filesystem access',
|
|
226
|
+
types: ['filesystemAccess'],
|
|
227
|
+
task: 'Resolve the path against a declared root and refuse a path that leaves it.',
|
|
228
|
+
},
|
|
229
|
+
network: {
|
|
230
|
+
label: 'Network access and embedded URLs',
|
|
231
|
+
types: ['networkAccess', 'urlStrings'],
|
|
232
|
+
task: 'Move the URL or address into configuration, or confirm it is a public endpoint the code must reach.',
|
|
233
|
+
},
|
|
234
|
+
shell: {
|
|
235
|
+
label: 'Shell access',
|
|
236
|
+
types: ['shellAccess'],
|
|
237
|
+
task: 'Quote every argument with shellArgumentFactory and run the command through shellExec, so the execution profile gates it.',
|
|
238
|
+
},
|
|
239
|
+
other: { label: 'Other', types: [], task: 'Review the flagged code and record the decision.' },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Reachability evidence a finding carries in the report. */
|
|
243
|
+
static REACHABILITY_TIERS = {
|
|
244
|
+
1: 'Full application reachability: analyzed against this code base (`socket scan create --reach`).',
|
|
245
|
+
2: 'Package-level reachability: a Socket scan alert; the package analysis is on the Socket dashboard.',
|
|
246
|
+
3: 'No reachability evidence: a registry advisory or a patch listing only. Treat as reachable.',
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolves the Socket configuration from the environment chain.
|
|
251
|
+
* @param {Function} [read] - Environment reader.
|
|
252
|
+
* @returns {{token: string, orgSlug: string, acceptRisks: string}}
|
|
253
|
+
*/
|
|
254
|
+
static socketEnvFactory(read = environmentValueFactory) {
|
|
255
|
+
const { token, orgSlug, acceptRisks } = SocketSecurityService.ENV;
|
|
256
|
+
return { token: read(token), orgSlug: read(orgSlug), acceptRisks: read(acceptRisks) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The `--org` arguments for commands that take one.
|
|
261
|
+
* @param {{orgSlug?: string}} [env]
|
|
262
|
+
* @returns {string[]}
|
|
263
|
+
*/
|
|
264
|
+
static orgArgsFactory(env = {}) {
|
|
265
|
+
return env.orgSlug ? ['--org', env.orgSlug] : [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Environment for a Socket child process: the current one plus the resolved Socket values,
|
|
270
|
+
* so a token read from an env file reaches the CLI without appearing on a command line.
|
|
271
|
+
* @param {{token?: string, orgSlug?: string, acceptRisks?: string}} env
|
|
272
|
+
* @param {NodeJS.ProcessEnv} [base]
|
|
273
|
+
* @returns {NodeJS.ProcessEnv}
|
|
274
|
+
*/
|
|
275
|
+
static childEnvFactory(env = {}, base = process.env) {
|
|
276
|
+
const values = Object.entries(SocketSecurityService.ENV)
|
|
277
|
+
.filter(([key]) => env[key])
|
|
278
|
+
.map(([key, name]) => [name, env[key]]);
|
|
279
|
+
return { ...base, ...Object.fromEntries(values) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Locates the Socket CLI: the project's own install first, then the PATH.
|
|
284
|
+
* @param {{root?: string, execute?: Function}} [options]
|
|
285
|
+
* @returns {string} Binary path, empty when Socket is not installed.
|
|
286
|
+
*/
|
|
287
|
+
static socketBinaryFactory({ root = process.cwd(), execute = shellExec } = {}) {
|
|
288
|
+
const local = nodePath.join(root, 'node_modules', '.bin', 'socket');
|
|
289
|
+
if (fs.existsSync(local)) return local;
|
|
290
|
+
const found = execute('command -v socket', { silent: true, silentOnError: true, stdout: true, disableLog: true });
|
|
291
|
+
return `${found ?? ''}`.trim().split('\n')[0];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Builds one Socket command line.
|
|
296
|
+
* @param {string[]} args
|
|
297
|
+
* @param {{binary?: string}} [options]
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
static socketCommandFactory(args = [], { binary = 'socket' } = {}) {
|
|
301
|
+
return [binary, ...args].map(shellArgumentFactory).join(' ');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Parses the JSON document a command printed, skipping the lines before it.
|
|
306
|
+
* @param {string} text
|
|
307
|
+
* @returns {object|Array|null}
|
|
308
|
+
*/
|
|
309
|
+
static parseJsonOutput(text = '') {
|
|
310
|
+
const source = `${text ?? ''}`;
|
|
311
|
+
for (const { index } of source.matchAll(/^[[{]/gm)) {
|
|
312
|
+
try {
|
|
313
|
+
return JSON.parse(source.slice(index));
|
|
314
|
+
} catch {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Normalizes a Socket command result. The main CLI prints an `{ok, message, cause, data}`
|
|
323
|
+
* envelope; the patch commands print a bare object with a `status`.
|
|
324
|
+
* @param {{code?: number, stdout?: string, stderr?: string}} [execution]
|
|
325
|
+
* @returns {{ok: boolean, code: number, data: *, message: string}}
|
|
326
|
+
*/
|
|
327
|
+
static socketResultFactory({ code = 0, stdout = '', stderr = '' } = {}) {
|
|
328
|
+
const parsed = SocketSecurityService.parseJsonOutput(stdout);
|
|
329
|
+
const envelope = parsed !== null && typeof parsed === 'object' && 'ok' in parsed;
|
|
330
|
+
const ok = envelope ? parsed.ok === true : code === 0 && parsed?.status !== 'error';
|
|
331
|
+
const failure = envelope
|
|
332
|
+
? [parsed.message, parsed.cause].filter(Boolean).join(': ')
|
|
333
|
+
: [parsed?.error, ...(parsed?.results ?? []).map((entry) => entry?.error)].filter(Boolean).join('; ') ||
|
|
334
|
+
`${stderr}`.trim().split('\n').filter(Boolean).pop() ||
|
|
335
|
+
'';
|
|
336
|
+
return { ok, code, data: envelope ? (parsed.data ?? null) : parsed, message: ok ? '' : failure };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Runs one Socket command in a checkout and returns its normalized result.
|
|
341
|
+
* @param {string[]} args
|
|
342
|
+
* @param {object} [options]
|
|
343
|
+
* @param {string} [options.root] - Checkout to run in.
|
|
344
|
+
* @param {Function} [options.execute] - Command executor.
|
|
345
|
+
* @param {object} [options.env] - Resolved Socket configuration.
|
|
346
|
+
* @param {boolean} [options.silent] - Hide the live output; long-running commands show it.
|
|
347
|
+
* @param {string} [options.binary] - Socket binary; resolved when absent.
|
|
348
|
+
* @returns {{ok: boolean, code: number, data: *, message: string, unavailable?: boolean}}
|
|
349
|
+
*/
|
|
350
|
+
static runSocket(
|
|
351
|
+
args,
|
|
352
|
+
{
|
|
353
|
+
root = process.cwd(),
|
|
354
|
+
execute = shellExec,
|
|
355
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
356
|
+
silent = true,
|
|
357
|
+
binary = SocketSecurityService.socketBinaryFactory({ root, execute }),
|
|
358
|
+
} = {},
|
|
359
|
+
) {
|
|
360
|
+
if (!binary)
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
code: 127,
|
|
364
|
+
data: null,
|
|
365
|
+
message: `${SocketSecurityService.CLI_PACKAGE} is not installed`,
|
|
366
|
+
unavailable: true,
|
|
367
|
+
};
|
|
368
|
+
const result = execute(SocketSecurityService.socketCommandFactory(args, { binary }), {
|
|
369
|
+
cwd: root,
|
|
370
|
+
silent,
|
|
371
|
+
silentOnError: true,
|
|
372
|
+
env: SocketSecurityService.childEnvFactory(env),
|
|
373
|
+
});
|
|
374
|
+
return SocketSecurityService.socketResultFactory(result);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The installed Socket CLI version, empty when it is not installed.
|
|
379
|
+
* @param {{root?: string, execute?: Function}} [options]
|
|
380
|
+
* @returns {string}
|
|
381
|
+
*/
|
|
382
|
+
static socketVersion(options = {}) {
|
|
383
|
+
const binary = SocketSecurityService.socketBinaryFactory(options);
|
|
384
|
+
if (!binary) return '';
|
|
385
|
+
const { execute = shellExec, root = process.cwd() } = options;
|
|
386
|
+
const output = execute(SocketSecurityService.socketCommandFactory(['--version', '--no-banner'], { binary }), {
|
|
387
|
+
cwd: root,
|
|
388
|
+
silent: true,
|
|
389
|
+
silentOnError: true,
|
|
390
|
+
stdout: true,
|
|
391
|
+
disableLog: true,
|
|
392
|
+
});
|
|
393
|
+
return `${output ?? ''}`.trim().split('\n').pop() ?? '';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Sort rank of a severity: most severe first, unknown last.
|
|
398
|
+
* @param {string} [severity]
|
|
399
|
+
* @returns {number}
|
|
400
|
+
*/
|
|
401
|
+
static severityRank(severity = '') {
|
|
402
|
+
const rank = SocketSecurityService.SEVERITIES.indexOf(`${severity}`.toLowerCase());
|
|
403
|
+
return rank === -1 ? SocketSecurityService.SEVERITIES.length : rank;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Summarizes an `npm audit --json` document.
|
|
408
|
+
* @param {object} [audit]
|
|
409
|
+
* @returns {{counts: object, dependencies: object, findings: object[]}}
|
|
410
|
+
*/
|
|
411
|
+
static npmAuditSummaryFactory(audit = {}) {
|
|
412
|
+
const counts = {
|
|
413
|
+
...Object.fromEntries(SocketSecurityService.SEVERITIES.map((severity) => [severity, 0])),
|
|
414
|
+
total: 0,
|
|
415
|
+
...(audit.metadata?.vulnerabilities ?? {}),
|
|
416
|
+
};
|
|
417
|
+
const fixLabel = (fix) => {
|
|
418
|
+
if (!fix) return 'none';
|
|
419
|
+
if (fix === true) return 'update';
|
|
420
|
+
return `${fix.name}@${fix.version}${fix.isSemVerMajor ? ' (major)' : ''}`;
|
|
421
|
+
};
|
|
422
|
+
const findings = Object.values(audit.vulnerabilities ?? {})
|
|
423
|
+
.map((vulnerability) => ({
|
|
424
|
+
name: vulnerability.name,
|
|
425
|
+
severity: vulnerability.severity,
|
|
426
|
+
range: vulnerability.range,
|
|
427
|
+
direct: vulnerability.isDirect === true,
|
|
428
|
+
fix: fixLabel(vulnerability.fixAvailable),
|
|
429
|
+
advisories: (vulnerability.via ?? [])
|
|
430
|
+
.filter((via) => typeof via === 'object')
|
|
431
|
+
.map(({ title, url, severity, range, cwe }) => ({ title, url, severity, range, cwe: cwe ?? [] })),
|
|
432
|
+
via: (vulnerability.via ?? []).filter((via) => typeof via === 'string'),
|
|
433
|
+
}))
|
|
434
|
+
.sort((a, b) => SocketSecurityService.severityRank(a.severity) - SocketSecurityService.severityRank(b.severity));
|
|
435
|
+
return { counts, dependencies: audit.metadata?.dependencies ?? {}, findings };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Runs `npm audit` on a checkout. A non-zero exit reports findings, so it is not a failure.
|
|
440
|
+
* @param {{root?: string, execute?: Function}} [options]
|
|
441
|
+
* @returns {{raw: object, summary: object}}
|
|
442
|
+
*/
|
|
443
|
+
static npmAudit({ root = process.cwd(), execute = shellExec } = {}) {
|
|
444
|
+
const result = execute('npm audit --json', { cwd: root, silent: true, silentOnError: true });
|
|
445
|
+
const raw = SocketSecurityService.parseJsonOutput(result?.stdout) ?? {};
|
|
446
|
+
return { raw, summary: SocketSecurityService.npmAuditSummaryFactory(raw) };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Runs `npm audit fix` without `--force`: only semver-compatible updates. A non-zero exit
|
|
451
|
+
* means vulnerabilities remain, not that the updates failed.
|
|
452
|
+
* @param {{root?: string, execute?: Function}} [options]
|
|
453
|
+
* @returns {{ok: boolean, code: number, message: string}}
|
|
454
|
+
*/
|
|
455
|
+
static npmAuditFix({ root = process.cwd(), execute = shellExec } = {}) {
|
|
456
|
+
const code = execute('npm audit fix --no-fund', { cwd: root, silentOnError: true })?.code ?? 0;
|
|
457
|
+
return { ok: code === 0, code, message: code === 0 ? '' : 'vulnerabilities remain after the compatible updates' };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Flattens a `socket scan report` document into one row per alert.
|
|
462
|
+
* @param {object} [report] - Report data, folded per version.
|
|
463
|
+
* @returns {{healthy: boolean|null, scanId: string, orgSlug: string, url: string, alerts: object[]}}
|
|
464
|
+
*/
|
|
465
|
+
static scanReportSummaryFactory(report = {}) {
|
|
466
|
+
const alerts = [];
|
|
467
|
+
const walk = (node, keys) => {
|
|
468
|
+
if (node && typeof node === 'object' && 'type' in node && 'policy' in node) {
|
|
469
|
+
const [ecosystem, name, version, file] = keys;
|
|
470
|
+
alerts.push({ ecosystem, package: name, version, file, ...node });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
for (const [key, value] of Object.entries(node ?? {})) walk(value, [...keys, key]);
|
|
474
|
+
};
|
|
475
|
+
walk(report?.alerts ?? {}, []);
|
|
476
|
+
const { scanId = '', orgSlug = '' } = report ?? {};
|
|
477
|
+
return {
|
|
478
|
+
healthy: typeof report?.healthy === 'boolean' ? report.healthy : null,
|
|
479
|
+
scanId,
|
|
480
|
+
orgSlug,
|
|
481
|
+
url: scanId && orgSlug ? `${SocketSecurityService.DASHBOARD_URL}/${orgSlug}/sbom/${scanId}` : '',
|
|
482
|
+
alerts,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Reads the reachability analysis into one row per (package, advisory).
|
|
488
|
+
* @param {object} [facts] - The `.socket.facts.json` document.
|
|
489
|
+
* @returns {{available: boolean, analyzed: number, counts: object, results: object[]}}
|
|
490
|
+
*/
|
|
491
|
+
static reachabilityFactory(facts = null) {
|
|
492
|
+
const components = Array.isArray(facts?.components) ? facts.components : [];
|
|
493
|
+
const results = components.flatMap((component) =>
|
|
494
|
+
(component.reachability ?? []).map((entry) => {
|
|
495
|
+
const outcomes = (entry.reachability ?? []).map((result) => result.type);
|
|
496
|
+
return {
|
|
497
|
+
package: component.name,
|
|
498
|
+
version: component.version,
|
|
499
|
+
ghsaId: entry.ghsa_id,
|
|
500
|
+
reachable: outcomes.includes('reachable')
|
|
501
|
+
? true
|
|
502
|
+
: outcomes.length > 0 && outcomes.every((type) => type === 'unreachable')
|
|
503
|
+
? false
|
|
504
|
+
: null,
|
|
505
|
+
outcomes,
|
|
506
|
+
};
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
const counts = { reachable: 0, unreachable: 0, undetermined: 0 };
|
|
510
|
+
for (const { reachable } of results)
|
|
511
|
+
counts[reachable === true ? 'reachable' : reachable === false ? 'unreachable' : 'undetermined']++;
|
|
512
|
+
return { available: facts !== null, analyzed: components.length, counts, results };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Creates a Socket full scan of a checkout, reports it against the organization policy, and
|
|
517
|
+
* reads its artifacts: every package with its per-file alerts.
|
|
518
|
+
*
|
|
519
|
+
* The scan uploads dependency manifests only, so its artifacts are the packages the checkout
|
|
520
|
+
* installs; this project's own code is read from the registry by {@link packageAlerts}. With
|
|
521
|
+
* `reach`, the reachability analysis runs locally over the working tree. A facts file left by
|
|
522
|
+
* an earlier run is removed before it starts, because the CLI takes one it finds as
|
|
523
|
+
* pre-generated input instead of analyzing, and the fresh one is consumed after it.
|
|
524
|
+
* @param {object} [options]
|
|
525
|
+
* @param {string} [options.root]
|
|
526
|
+
* @param {Function} [options.execute]
|
|
527
|
+
* @param {object} [options.env]
|
|
528
|
+
* @param {string} [options.binary] - Socket binary; resolved when absent.
|
|
529
|
+
* @param {boolean} [options.reach]
|
|
530
|
+
* @param {string} [options.repo]
|
|
531
|
+
* @param {string} [options.branch]
|
|
532
|
+
* @param {string[]} [options.targets]
|
|
533
|
+
* @returns {{ok: boolean, message: string, summary: object, artifacts: object[]|null, report: object|null, facts: object|null, reachability: object}}
|
|
534
|
+
*/
|
|
535
|
+
static scan({
|
|
536
|
+
root = process.cwd(),
|
|
537
|
+
execute = shellExec,
|
|
538
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
539
|
+
binary,
|
|
540
|
+
reach = false,
|
|
541
|
+
repo = '',
|
|
542
|
+
branch = '',
|
|
543
|
+
targets = ['.'],
|
|
544
|
+
} = {}) {
|
|
545
|
+
const factsPath = nodePath.join(root, SocketSecurityService.FACTS_PATH);
|
|
546
|
+
if (reach) fs.removeSync(factsPath);
|
|
547
|
+
const result = SocketSecurityService.runSocket(
|
|
548
|
+
[
|
|
549
|
+
'scan',
|
|
550
|
+
'create',
|
|
551
|
+
'--json',
|
|
552
|
+
'--report',
|
|
553
|
+
'--report-level',
|
|
554
|
+
SocketSecurityService.REPORT_LEVEL,
|
|
555
|
+
'--no-interactive',
|
|
556
|
+
'--no-banner',
|
|
557
|
+
...(reach ? ['--reach', '--reach-retain-facts-file'] : []),
|
|
558
|
+
...(repo ? ['--repo', repo] : []),
|
|
559
|
+
...(branch ? ['--branch', branch] : []),
|
|
560
|
+
...SocketSecurityService.orgArgsFactory(env),
|
|
561
|
+
...targets,
|
|
562
|
+
],
|
|
563
|
+
{ root, execute, env, binary, silent: false },
|
|
564
|
+
);
|
|
565
|
+
const facts = reach && fs.existsSync(factsPath) ? fs.readJsonSync(factsPath, { throws: false }) : null;
|
|
566
|
+
if (facts !== null) fs.removeSync(factsPath);
|
|
567
|
+
const summary = SocketSecurityService.scanReportSummaryFactory(result.data);
|
|
568
|
+
const view =
|
|
569
|
+
result.ok && summary.scanId
|
|
570
|
+
? SocketSecurityService.runSocket(
|
|
571
|
+
['scan', 'view', summary.scanId, '--json', '--no-banner', ...SocketSecurityService.orgArgsFactory(env)],
|
|
572
|
+
{ root, execute, env, binary },
|
|
573
|
+
)
|
|
574
|
+
: null;
|
|
575
|
+
return {
|
|
576
|
+
ok: result.ok && view?.ok !== false,
|
|
577
|
+
message: result.message || view?.message || '',
|
|
578
|
+
unavailable: result.unavailable === true,
|
|
579
|
+
summary,
|
|
580
|
+
artifacts: Array.isArray(view?.data) ? view.data : null,
|
|
581
|
+
report: result.data,
|
|
582
|
+
facts,
|
|
583
|
+
reachability: SocketSecurityService.reachabilityFactory(facts),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The registry coordinates of this project's own published packages: each own name at the
|
|
589
|
+
* checked-out version when that version is published, at its latest version otherwise, and
|
|
590
|
+
* left out when the registry does not carry it.
|
|
591
|
+
* @param {object} [params]
|
|
592
|
+
* @param {object} [params.packageJson]
|
|
593
|
+
* @param {string[]} [params.ownPackages] - `ownPackageNamesFactory` result.
|
|
594
|
+
* @param {string} [params.root]
|
|
595
|
+
* @param {Function} [params.execute]
|
|
596
|
+
* @returns {Array<{name: string, version: string, current: boolean, purl: string}>}
|
|
597
|
+
*/
|
|
598
|
+
static publishedPackagesFactory({
|
|
599
|
+
packageJson = {},
|
|
600
|
+
ownPackages = [],
|
|
601
|
+
root = process.cwd(),
|
|
602
|
+
execute = shellExec,
|
|
603
|
+
} = {}) {
|
|
604
|
+
const view = (spec) =>
|
|
605
|
+
`${
|
|
606
|
+
execute(`npm view ${shellArgumentFactory(spec)} version`, {
|
|
607
|
+
cwd: root,
|
|
608
|
+
silent: true,
|
|
609
|
+
silentOnError: true,
|
|
610
|
+
stdout: true,
|
|
611
|
+
disableLog: true,
|
|
612
|
+
}) ?? ''
|
|
613
|
+
}`
|
|
614
|
+
.trim()
|
|
615
|
+
.split('\n')
|
|
616
|
+
.pop()
|
|
617
|
+
.replace(/^'|'$/g, '');
|
|
618
|
+
return ownPackages.flatMap((name) => {
|
|
619
|
+
const current = packageJson.version ? view(`${name}@${packageJson.version}`) : '';
|
|
620
|
+
const version = current || view(name);
|
|
621
|
+
if (!/^\d+\.\d+\.\d+/.test(version)) return [];
|
|
622
|
+
return [{ name, version, current: version === packageJson.version, purl: `pkg:npm/${name}@${version}` }];
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Reads the alerts Socket raises on published packages from the registry analysis of their
|
|
628
|
+
* tarballs: the same per-file alerts, in the same artifact shape, as a full scan's artifacts.
|
|
629
|
+
* @param {object} [options]
|
|
630
|
+
* @param {string} [options.root]
|
|
631
|
+
* @param {Function} [options.execute]
|
|
632
|
+
* @param {object} [options.env]
|
|
633
|
+
* @param {string} [options.binary] - Socket binary; resolved when absent.
|
|
634
|
+
* @param {Array<{purl: string}>} [options.packages] - `publishedPackagesFactory` result.
|
|
635
|
+
* @returns {{ok: boolean, message: string, unavailable: boolean, packages: object[], artifacts: object[]|null}}
|
|
636
|
+
*/
|
|
637
|
+
static packageAlerts({
|
|
638
|
+
root = process.cwd(),
|
|
639
|
+
execute = shellExec,
|
|
640
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
641
|
+
binary,
|
|
642
|
+
packages = [],
|
|
643
|
+
} = {}) {
|
|
644
|
+
if (packages.length === 0) return { ok: true, message: '', unavailable: false, packages, artifacts: [] };
|
|
645
|
+
const result = SocketSecurityService.runSocket(
|
|
646
|
+
['package', 'shallow', ...packages.map(({ purl }) => purl), '--json', '--no-banner'],
|
|
647
|
+
{ root, execute, env, binary },
|
|
648
|
+
);
|
|
649
|
+
const data = Array.isArray(result.data) ? result.data : result.data?.data;
|
|
650
|
+
return {
|
|
651
|
+
ok: result.ok && Array.isArray(data),
|
|
652
|
+
message: result.message || (result.ok && !Array.isArray(data) ? 'the package lookup returned no packages' : ''),
|
|
653
|
+
unavailable: result.unavailable === true,
|
|
654
|
+
packages,
|
|
655
|
+
artifacts: Array.isArray(data) ? data : null,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Summarizes a `socket patch scan --json` document.
|
|
661
|
+
* @param {object} [data]
|
|
662
|
+
* @returns {{scannedPackages: number, packagesWithPatches: number, freePatches: number, paidPatches: number, canAccessPaidPatches: boolean, available: object[]}}
|
|
663
|
+
*/
|
|
664
|
+
static patchScanSummaryFactory(data = {}) {
|
|
665
|
+
const available = (data?.packages ?? [])
|
|
666
|
+
.flatMap((entry) => entry.patches ?? [])
|
|
667
|
+
.map(({ purl, uuid, severity, tier, title, cveIds, ghsaIds }) => ({
|
|
668
|
+
purl,
|
|
669
|
+
uuid,
|
|
670
|
+
severity: `${severity ?? ''}`.toLowerCase(),
|
|
671
|
+
tier,
|
|
672
|
+
title,
|
|
673
|
+
cveIds: cveIds ?? [],
|
|
674
|
+
ghsaIds: ghsaIds ?? [],
|
|
675
|
+
}))
|
|
676
|
+
.sort((a, b) => SocketSecurityService.severityRank(a.severity) - SocketSecurityService.severityRank(b.severity));
|
|
677
|
+
return {
|
|
678
|
+
scannedPackages: data?.scannedPackages ?? 0,
|
|
679
|
+
packagesWithPatches: data?.packagesWithPatches ?? 0,
|
|
680
|
+
freePatches: data?.freePatches ?? 0,
|
|
681
|
+
paidPatches: data?.paidPatches ?? 0,
|
|
682
|
+
canAccessPaidPatches: data?.canAccessPaidPatches === true,
|
|
683
|
+
available,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Chooses the one patch a package version can carry: the manifest records a single patch
|
|
689
|
+
* per PURL, and two patches on the same files cannot both verify. The most severe wins, then
|
|
690
|
+
* the newest, which is the order Socket lists them in.
|
|
691
|
+
* @param {object[]} [available] - `patchScanSummaryFactory().available`.
|
|
692
|
+
* @returns {{selected: object[], skipped: object[]}}
|
|
693
|
+
*/
|
|
694
|
+
static patchSelectionFactory(available = []) {
|
|
695
|
+
const selected = new Map();
|
|
696
|
+
const skipped = [];
|
|
697
|
+
for (const patch of available) {
|
|
698
|
+
const chosen = selected.get(patch.purl);
|
|
699
|
+
if (!chosen) selected.set(patch.purl, patch);
|
|
700
|
+
else skipped.push({ ...patch, reason: `one patch per package version: ${chosen.uuid} selected` });
|
|
701
|
+
}
|
|
702
|
+
return { selected: [...selected.values()], skipped };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Scans the installed packages for available Socket patches. Without a token the public
|
|
707
|
+
* patch API answers with the free tier only.
|
|
708
|
+
* @param {{root?: string, execute?: Function, env?: object, binary?: string}} [options]
|
|
709
|
+
* @returns {{ok: boolean, message: string, unavailable: boolean, data: object|null, summary: object}}
|
|
710
|
+
*/
|
|
711
|
+
static patchScan({
|
|
712
|
+
root = process.cwd(),
|
|
713
|
+
execute = shellExec,
|
|
714
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
715
|
+
binary,
|
|
716
|
+
} = {}) {
|
|
717
|
+
const result = SocketSecurityService.runSocket(
|
|
718
|
+
['patch', 'scan', '--json', '--yes', ...SocketSecurityService.orgArgsFactory(env)],
|
|
719
|
+
{ root, execute, env, binary },
|
|
720
|
+
);
|
|
721
|
+
return {
|
|
722
|
+
ok: result.ok,
|
|
723
|
+
message: result.message,
|
|
724
|
+
unavailable: result.unavailable === true,
|
|
725
|
+
data: result.data,
|
|
726
|
+
summary: SocketSecurityService.patchScanSummaryFactory(result.data),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Downloads one patch into the manifest and applies it.
|
|
732
|
+
* @param {{identifier: string, root?: string, execute?: Function, env?: object, binary?: string}} options
|
|
733
|
+
* @returns {{ok: boolean, message: string, data: object|null}}
|
|
734
|
+
*/
|
|
735
|
+
static patchGet({
|
|
736
|
+
identifier,
|
|
737
|
+
root = process.cwd(),
|
|
738
|
+
execute = shellExec,
|
|
739
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
740
|
+
binary,
|
|
741
|
+
} = {}) {
|
|
742
|
+
if (!identifier) throw new TypeError('patchGet requires a patch UUID, CVE, GHSA or PURL');
|
|
743
|
+
const result = SocketSecurityService.runSocket(
|
|
744
|
+
['patch', 'get', identifier, '--yes', '--json', ...SocketSecurityService.orgArgsFactory(env)],
|
|
745
|
+
{ root, execute, env, binary },
|
|
746
|
+
);
|
|
747
|
+
return { ok: result.ok, message: result.message, data: result.data };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Applies the patches the manifest records. Idempotent: an applied patch is verified, a
|
|
752
|
+
* package whose version moved is skipped, a checkout without a manifest is a no-op.
|
|
753
|
+
* @param {{root?: string, execute?: Function, env?: object, binary?: string, dryRun?: boolean}} [options]
|
|
754
|
+
* @returns {{ok: boolean, message: string, unavailable: boolean, status: string, patchesApplied: number, alreadyPatched: number, failed: number, results: object[]}}
|
|
755
|
+
*/
|
|
756
|
+
static patchApply({
|
|
757
|
+
root = process.cwd(),
|
|
758
|
+
execute = shellExec,
|
|
759
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
760
|
+
binary,
|
|
761
|
+
dryRun = false,
|
|
762
|
+
} = {}) {
|
|
763
|
+
const result = SocketSecurityService.runSocket(
|
|
764
|
+
[...SocketSecurityService.PATCH_APPLY_ARGS, '--json', ...(dryRun ? ['--dry-run'] : [])],
|
|
765
|
+
{ root, execute, env, binary },
|
|
766
|
+
);
|
|
767
|
+
const data = result.data ?? {};
|
|
768
|
+
return {
|
|
769
|
+
ok: result.ok,
|
|
770
|
+
message: result.message,
|
|
771
|
+
unavailable: result.unavailable === true,
|
|
772
|
+
status: result.unavailable ? 'unavailable' : (data.status ?? 'unknown'),
|
|
773
|
+
patchesApplied: data.patchesApplied ?? 0,
|
|
774
|
+
alreadyPatched: data.alreadyPatched ?? 0,
|
|
775
|
+
failed: data.failed ?? 0,
|
|
776
|
+
results: data.results ?? [],
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Reads the patch manifest of a checkout.
|
|
782
|
+
* @param {string} [root]
|
|
783
|
+
* @returns {{path: string, exists: boolean, patches: object[]}}
|
|
784
|
+
*/
|
|
785
|
+
static patchManifestFactory(root = process.cwd()) {
|
|
786
|
+
const path = nodePath.join(root, SocketSecurityService.MANIFEST_PATH);
|
|
787
|
+
if (!fs.existsSync(path)) return { path: SocketSecurityService.MANIFEST_PATH, exists: false, patches: [] };
|
|
788
|
+
const { patches = {} } = fs.readJsonSync(path);
|
|
789
|
+
return {
|
|
790
|
+
path: SocketSecurityService.MANIFEST_PATH,
|
|
791
|
+
exists: true,
|
|
792
|
+
patches: Object.entries(patches).map(([purl, patch]) => ({
|
|
793
|
+
purl,
|
|
794
|
+
uuid: patch.uuid,
|
|
795
|
+
tier: patch.tier,
|
|
796
|
+
license: patch.license,
|
|
797
|
+
files: Object.keys(patch.files ?? {}),
|
|
798
|
+
vulnerabilities: Object.entries(patch.vulnerabilities ?? {}).map(([id, vulnerability]) => ({
|
|
799
|
+
id,
|
|
800
|
+
severity: `${vulnerability.severity ?? ''}`.toLowerCase(),
|
|
801
|
+
summary: vulnerability.summary,
|
|
802
|
+
cves: vulnerability.cves ?? [],
|
|
803
|
+
})),
|
|
804
|
+
})),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Installs the `prepare` hook that applies the manifest on every install of the checkout.
|
|
810
|
+
* Idempotent, and it keeps the manifest's key order and indentation.
|
|
811
|
+
* @param {{root?: string, dryRun?: boolean}} [options]
|
|
812
|
+
* @returns {{path: string, prepare: string, changed: boolean}}
|
|
813
|
+
*/
|
|
814
|
+
static patchSetup({ root = process.cwd(), dryRun = false } = {}) {
|
|
815
|
+
const path = nodePath.join(root, 'package.json');
|
|
816
|
+
const source = fs.readFileSync(path, 'utf8');
|
|
817
|
+
const manifest = JSON.parse(source);
|
|
818
|
+
const current = `${manifest.scripts?.prepare ?? ''}`.trim();
|
|
819
|
+
const prepare = current.includes(SocketSecurityService.PREPARE_SCRIPT)
|
|
820
|
+
? current
|
|
821
|
+
: [current, SocketSecurityService.PREPARE_SCRIPT].filter(Boolean).join(' && ');
|
|
822
|
+
const changed = prepare !== current;
|
|
823
|
+
if (changed && !dryRun) {
|
|
824
|
+
manifest.scripts = { ...manifest.scripts, prepare };
|
|
825
|
+
fs.writeFileSync(path, `${JSON.stringify(manifest, null, /^(\s+)"/m.exec(source)?.[1] ?? ' ')}\n`, 'utf8');
|
|
826
|
+
}
|
|
827
|
+
return { path, prepare, changed };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Runs `socket fix`: dependency upgrades that resolve known CVEs, semver-compatible unless
|
|
832
|
+
* `majorUpdates` is set.
|
|
833
|
+
* @param {{root?: string, execute?: Function, env?: object, binary?: string, majorUpdates?: boolean, apply?: boolean, outputFile?: string}} [options]
|
|
834
|
+
* @returns {{ok: boolean, message: string, unavailable: boolean, data: *}}
|
|
835
|
+
*/
|
|
836
|
+
static fix({
|
|
837
|
+
root = process.cwd(),
|
|
838
|
+
execute = shellExec,
|
|
839
|
+
env = SocketSecurityService.socketEnvFactory(),
|
|
840
|
+
binary,
|
|
841
|
+
majorUpdates = false,
|
|
842
|
+
apply = true,
|
|
843
|
+
outputFile = '',
|
|
844
|
+
} = {}) {
|
|
845
|
+
const result = SocketSecurityService.runSocket(
|
|
846
|
+
[
|
|
847
|
+
'fix',
|
|
848
|
+
'--json',
|
|
849
|
+
'--no-banner',
|
|
850
|
+
...(majorUpdates ? [] : ['--no-major-updates']),
|
|
851
|
+
...(apply ? [] : ['--no-apply-fixes']),
|
|
852
|
+
...(outputFile ? ['--output-file', outputFile] : []),
|
|
853
|
+
],
|
|
854
|
+
{ root, execute, env, binary, silent: false },
|
|
855
|
+
);
|
|
856
|
+
return { ok: result.ok, message: result.message, unavailable: result.unavailable === true, data: result.data };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* The package name and version an npm PURL names.
|
|
861
|
+
* @param {string} [purl]
|
|
862
|
+
* @returns {{name: string, version: string}}
|
|
863
|
+
*/
|
|
864
|
+
static purlPackageFactory(purl = '') {
|
|
865
|
+
const [, name = '', version = ''] = /^pkg:npm\/(.+)@([^@]+)$/.exec(`${purl ?? ''}`) ?? [];
|
|
866
|
+
return { name, version };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* The GHSA id an advisory URL or id list names.
|
|
871
|
+
* @param {string} [text]
|
|
872
|
+
* @returns {string}
|
|
873
|
+
*/
|
|
874
|
+
static ghsaIdFactory(text = '') {
|
|
875
|
+
const match = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/i.exec(`${text ?? ''}`)?.[0] ?? '';
|
|
876
|
+
return match.replace(/^ghsa/i, 'GHSA');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* The category key of a Socket alert type.
|
|
881
|
+
* @param {string} [type]
|
|
882
|
+
* @returns {string}
|
|
883
|
+
*/
|
|
884
|
+
static alertCategoryFactory(type = '') {
|
|
885
|
+
const entry = Object.entries(SocketSecurityService.ALERT_CATEGORIES).find(([, category]) =>
|
|
886
|
+
category.types.includes(type),
|
|
887
|
+
);
|
|
888
|
+
return entry ? entry[0] : 'other';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* The package names this project publishes its own code under: the manifest name and the bin
|
|
893
|
+
* names, bare and under the repository owner and organization scopes.
|
|
894
|
+
* @param {object} [params]
|
|
895
|
+
* @param {object} [params.packageJson]
|
|
896
|
+
* @param {string} [params.organization] - `GITHUB_ORG_NAME` otherwise.
|
|
897
|
+
* @returns {string[]}
|
|
898
|
+
*/
|
|
899
|
+
static ownPackageNamesFactory({ packageJson = {}, organization = environmentValueFactory('GITHUB_ORG_NAME') } = {}) {
|
|
900
|
+
const repository = `${packageJson.repository?.url ?? packageJson.repository ?? ''}`;
|
|
901
|
+
const owners = [/github\.com[/:]([^/]+)\//.exec(repository)?.[1], organization].filter(Boolean);
|
|
902
|
+
const names = [packageJson.name, ...Object.keys(packageJson.bin ?? {})].filter(Boolean);
|
|
903
|
+
const scoped = [...new Set(owners)].flatMap((owner) => names.map((name) => `@${owner}/${name}`));
|
|
904
|
+
return [...new Set([...names, ...scoped])];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* The full package name of a scan artifact.
|
|
909
|
+
* @param {{namespace?: string, name?: string}} [artifact]
|
|
910
|
+
* @returns {string}
|
|
911
|
+
*/
|
|
912
|
+
static artifactNameFactory(artifact = {}) {
|
|
913
|
+
const scope = `${artifact.namespace ?? ''}`;
|
|
914
|
+
return scope ? `${scope.startsWith('@') ? scope : `@${scope}`}/${artifact.name}` : `${artifact.name ?? ''}`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Maps an alert file to this checkout. The npm tarball prefix is dropped, a path that leaves
|
|
919
|
+
* the root is refused, and the `start` offset becomes a line only when the analyzed version is
|
|
920
|
+
* the checked-out one.
|
|
921
|
+
* @param {object} [params]
|
|
922
|
+
* @param {string} [params.root]
|
|
923
|
+
* @param {string} [params.file] - Alert file path.
|
|
924
|
+
* @param {number} [params.start] - Character offset of the alert.
|
|
925
|
+
* @param {boolean} [params.current] - The analyzed version is the checked-out version.
|
|
926
|
+
* @returns {{file: string, line: number|null, local: boolean}}
|
|
927
|
+
*/
|
|
928
|
+
static sourceLocationFactory({ root = process.cwd(), file = '', start, current = true } = {}) {
|
|
929
|
+
const path = `${file ?? ''}`.replace(/^package\//, '');
|
|
930
|
+
const base = nodePath.resolve(root);
|
|
931
|
+
const absolute = nodePath.resolve(base, path);
|
|
932
|
+
const inside = Boolean(path) && absolute.startsWith(`${base}${nodePath.sep}`);
|
|
933
|
+
const local = inside && fs.existsSync(absolute) && fs.statSync(absolute).isFile();
|
|
934
|
+
let line = null;
|
|
935
|
+
if (local && current && Number.isInteger(start)) {
|
|
936
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
937
|
+
if (start <= content.length) line = content.slice(0, start).split('\n').length;
|
|
938
|
+
}
|
|
939
|
+
return { file: inside ? path : '', line, local };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* A finding location as `file:line`, or the package when the alert names no file.
|
|
944
|
+
* @param {{file?: string, line?: number|null, local?: boolean, package?: string, installed?: string}} finding
|
|
945
|
+
* @returns {string}
|
|
946
|
+
*/
|
|
947
|
+
static locationLabelFactory(finding = {}) {
|
|
948
|
+
if (!finding.file) return `${finding.package}@${finding.installed}`;
|
|
949
|
+
return `${finding.file}${finding.line ? `:${finding.line}` : ''}${finding.local ? '' : ' (not in this checkout)'}`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Splits scan artifact alerts into the two report domains. An alert on one of this project's
|
|
954
|
+
* own packages is a source finding with a location; every other alert is a dependency finding,
|
|
955
|
+
* one per package version and alert type.
|
|
956
|
+
* @param {object} [params]
|
|
957
|
+
* @param {object[]} [params.artifacts] - `scan view` artifacts.
|
|
958
|
+
* @param {object[]} [params.policies] - `scanReportSummaryFactory().alerts`, for the policy action.
|
|
959
|
+
* @param {string[]} [params.ownPackages] - `ownPackageNamesFactory` result.
|
|
960
|
+
* @param {string} [params.root]
|
|
961
|
+
* @param {string} [params.version] - Checked-out version.
|
|
962
|
+
* @returns {{dependency: object[], source: object[]}}
|
|
963
|
+
*/
|
|
964
|
+
static alertFindingsFactory({
|
|
965
|
+
artifacts = [],
|
|
966
|
+
policies = [],
|
|
967
|
+
ownPackages = [],
|
|
968
|
+
root = process.cwd(),
|
|
969
|
+
version = '',
|
|
970
|
+
} = {}) {
|
|
971
|
+
const { severityRank, SOCKET_SEVERITIES } = SocketSecurityService;
|
|
972
|
+
const own = new Set(ownPackages);
|
|
973
|
+
const policy = new Map(policies.map((alert) => [`${alert.package}@${alert.version}#${alert.type}`, alert.policy]));
|
|
974
|
+
const dependency = new Map();
|
|
975
|
+
const source = new Map();
|
|
976
|
+
for (const artifact of artifacts) {
|
|
977
|
+
const name = SocketSecurityService.artifactNameFactory(artifact);
|
|
978
|
+
for (const alert of artifact.alerts ?? []) {
|
|
979
|
+
const finding = {
|
|
980
|
+
category: SocketSecurityService.alertCategoryFactory(alert.type),
|
|
981
|
+
type: alert.type,
|
|
982
|
+
severity: SOCKET_SEVERITIES[alert.severity] ?? 'info',
|
|
983
|
+
package: name,
|
|
984
|
+
installed: artifact.version,
|
|
985
|
+
policy: policy.get(`${name}@${artifact.version}#${alert.type}`) ?? alert.action ?? '',
|
|
986
|
+
};
|
|
987
|
+
if (!own.has(name)) {
|
|
988
|
+
const key = `${name}@${artifact.version}#${alert.type}`;
|
|
989
|
+
const current = dependency.get(key);
|
|
990
|
+
dependency.set(key, {
|
|
991
|
+
...finding,
|
|
992
|
+
severity:
|
|
993
|
+
current && severityRank(current.severity) < severityRank(finding.severity)
|
|
994
|
+
? current.severity
|
|
995
|
+
: finding.severity,
|
|
996
|
+
files: (current?.files ?? 0) + (alert.file ? 1 : 0),
|
|
997
|
+
});
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const location = SocketSecurityService.sourceLocationFactory({
|
|
1001
|
+
root,
|
|
1002
|
+
file: alert.file,
|
|
1003
|
+
start: alert.start,
|
|
1004
|
+
current: artifact.version === version,
|
|
1005
|
+
});
|
|
1006
|
+
const key = `${alert.type}#${location.file}#${location.line ?? alert.start ?? ''}#${name}`;
|
|
1007
|
+
if (!source.has(key)) source.set(key, { ...finding, ...location });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const order = Object.keys(SocketSecurityService.ALERT_CATEGORIES);
|
|
1011
|
+
const sorted = (findings) =>
|
|
1012
|
+
findings.sort(
|
|
1013
|
+
(a, b) =>
|
|
1014
|
+
severityRank(a.severity) - severityRank(b.severity) ||
|
|
1015
|
+
order.indexOf(a.category) - order.indexOf(b.category) ||
|
|
1016
|
+
`${a.file ?? ''}${a.package}`.localeCompare(`${b.file ?? ''}${b.package}`) ||
|
|
1017
|
+
(a.line ?? 0) - (b.line ?? 0),
|
|
1018
|
+
);
|
|
1019
|
+
return { dependency: sorted([...dependency.values()]), source: sorted([...source.values()]) };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Findings per severity, with the total.
|
|
1024
|
+
* @param {object[]} [findings]
|
|
1025
|
+
* @returns {object}
|
|
1026
|
+
*/
|
|
1027
|
+
static severityCountsFactory(findings = []) {
|
|
1028
|
+
return {
|
|
1029
|
+
...Object.fromEntries(
|
|
1030
|
+
SocketSecurityService.SEVERITIES.map((severity) => [
|
|
1031
|
+
severity,
|
|
1032
|
+
findings.filter((finding) => finding.severity === severity).length,
|
|
1033
|
+
]),
|
|
1034
|
+
),
|
|
1035
|
+
total: findings.length,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Findings per category, in report order, without empty categories.
|
|
1041
|
+
* @param {object[]} [findings]
|
|
1042
|
+
* @returns {Object<string, number>}
|
|
1043
|
+
*/
|
|
1044
|
+
static categoryCountsFactory(findings = []) {
|
|
1045
|
+
return Object.fromEntries(
|
|
1046
|
+
Object.keys(SocketSecurityService.ALERT_CATEGORIES)
|
|
1047
|
+
.map((category) => [category, findings.filter((finding) => finding.category === category).length])
|
|
1048
|
+
.filter(([, count]) => count > 0),
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Groups source findings into one refactoring task per category, most severe first.
|
|
1054
|
+
* @param {object[]} [findings] - `alertFindingsFactory().source`.
|
|
1055
|
+
* @returns {Array<{category: string, label: string, task: string, severity: string, types: string[], locations: string[]}>}
|
|
1056
|
+
*/
|
|
1057
|
+
static sourceTasksFactory(findings = []) {
|
|
1058
|
+
const { ALERT_CATEGORIES, severityRank } = SocketSecurityService;
|
|
1059
|
+
const tasks = new Map();
|
|
1060
|
+
for (const finding of findings) {
|
|
1061
|
+
const task = tasks.get(finding.category) ?? {
|
|
1062
|
+
category: finding.category,
|
|
1063
|
+
label: ALERT_CATEGORIES[finding.category].label,
|
|
1064
|
+
task: ALERT_CATEGORIES[finding.category].task,
|
|
1065
|
+
severity: finding.severity,
|
|
1066
|
+
types: [],
|
|
1067
|
+
locations: [],
|
|
1068
|
+
};
|
|
1069
|
+
if (severityRank(finding.severity) < severityRank(task.severity)) task.severity = finding.severity;
|
|
1070
|
+
if (!task.types.includes(finding.type)) task.types.push(finding.type);
|
|
1071
|
+
const location = SocketSecurityService.locationLabelFactory(finding);
|
|
1072
|
+
if (!task.locations.includes(location)) task.locations.push(location);
|
|
1073
|
+
tasks.set(finding.category, task);
|
|
1074
|
+
}
|
|
1075
|
+
return [...tasks.values()].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Merges the dependency sources into one finding per (package, advisory or alert) and assigns
|
|
1080
|
+
* each its reachability tier; see {@link SocketSecurityService.REACHABILITY_TIERS}.
|
|
1081
|
+
* @param {object} [params]
|
|
1082
|
+
* @param {object} [params.npmAudit] - `npmAuditSummaryFactory` result.
|
|
1083
|
+
* @param {object} [params.patchScan] - `patchScanSummaryFactory` result.
|
|
1084
|
+
* @param {object[]} [params.alerts] - `alertFindingsFactory().dependency`.
|
|
1085
|
+
* @param {object} [params.reachability] - `reachabilityFactory` result.
|
|
1086
|
+
* @param {object} [params.manifest] - `patchManifestFactory` result.
|
|
1087
|
+
* @returns {object[]} Findings, most severe first.
|
|
1088
|
+
*/
|
|
1089
|
+
static findingsFactory({ npmAudit = {}, patchScan = {}, alerts = [], reachability = {}, manifest = {} } = {}) {
|
|
1090
|
+
const findings = new Map();
|
|
1091
|
+
const merge = (key, finding) => {
|
|
1092
|
+
const current = findings.get(key) ?? { sources: [], patches: [] };
|
|
1093
|
+
findings.set(key, {
|
|
1094
|
+
...current,
|
|
1095
|
+
...Object.fromEntries(Object.entries(finding).filter(([, value]) => value !== undefined && value !== '')),
|
|
1096
|
+
sources: [...new Set([...current.sources, finding.source])],
|
|
1097
|
+
patches: [...new Set([...current.patches, ...(finding.patches ?? [])])],
|
|
1098
|
+
});
|
|
1099
|
+
};
|
|
1100
|
+
for (const entry of npmAudit.findings ?? [])
|
|
1101
|
+
for (const advisory of entry.advisories) {
|
|
1102
|
+
const id = SocketSecurityService.ghsaIdFactory(advisory.url) || advisory.url;
|
|
1103
|
+
merge(`${entry.name}#${id}`, {
|
|
1104
|
+
package: entry.name,
|
|
1105
|
+
id,
|
|
1106
|
+
category: 'advisories',
|
|
1107
|
+
title: advisory.title,
|
|
1108
|
+
severity: advisory.severity,
|
|
1109
|
+
range: entry.range,
|
|
1110
|
+
direct: entry.direct,
|
|
1111
|
+
fix: entry.fix,
|
|
1112
|
+
source: 'npm audit',
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
for (const patch of patchScan.available ?? []) {
|
|
1116
|
+
const { name, version } = SocketSecurityService.purlPackageFactory(patch.purl);
|
|
1117
|
+
const id = patch.ghsaIds[0] ?? patch.cveIds[0] ?? patch.uuid;
|
|
1118
|
+
merge(`${name}#${id}`, {
|
|
1119
|
+
package: name,
|
|
1120
|
+
id,
|
|
1121
|
+
category: 'advisories',
|
|
1122
|
+
title: patch.title,
|
|
1123
|
+
severity: patch.severity,
|
|
1124
|
+
installed: version,
|
|
1125
|
+
patches: [patch.uuid],
|
|
1126
|
+
source: 'socket patch',
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
for (const alert of alerts)
|
|
1130
|
+
merge(`${alert.package}#${alert.type}@${alert.installed}`, {
|
|
1131
|
+
package: alert.package,
|
|
1132
|
+
id: alert.type,
|
|
1133
|
+
category: alert.category,
|
|
1134
|
+
severity: alert.severity,
|
|
1135
|
+
installed: alert.installed,
|
|
1136
|
+
policy: alert.policy,
|
|
1137
|
+
files: alert.files,
|
|
1138
|
+
source: 'socket scan',
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const evidence = new Map(
|
|
1142
|
+
(reachability.results ?? []).map((result) => [`${result.package}#${result.ghsaId}`, result.reachable]),
|
|
1143
|
+
);
|
|
1144
|
+
const patched = new Set(
|
|
1145
|
+
(manifest.patches ?? []).flatMap((patch) =>
|
|
1146
|
+
patch.vulnerabilities.map(({ id }) => `${SocketSecurityService.purlPackageFactory(patch.purl).name}#${id}`),
|
|
1147
|
+
),
|
|
1148
|
+
);
|
|
1149
|
+
return [...findings.entries()]
|
|
1150
|
+
.map(([key, finding]) => {
|
|
1151
|
+
const reachable = evidence.has(key) ? evidence.get(key) : null;
|
|
1152
|
+
const tier = evidence.has(key) ? 1 : finding.sources.includes('socket scan') ? 2 : 3;
|
|
1153
|
+
return { ...finding, tier, reachable, fix: finding.fix ?? 'none', patched: patched.has(key) };
|
|
1154
|
+
})
|
|
1155
|
+
.sort(
|
|
1156
|
+
(a, b) =>
|
|
1157
|
+
SocketSecurityService.severityRank(a.severity) - SocketSecurityService.severityRank(b.severity) ||
|
|
1158
|
+
a.package.localeCompare(b.package),
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* The findings no non-breaking action resolves, with the reason each one stays.
|
|
1164
|
+
* @param {object[]} [findings]
|
|
1165
|
+
* @returns {object[]}
|
|
1166
|
+
*/
|
|
1167
|
+
static deferredFactory(findings = []) {
|
|
1168
|
+
return findings
|
|
1169
|
+
.filter((finding) => !finding.patched && (finding.fix === 'none' || finding.fix.endsWith('(major)')))
|
|
1170
|
+
.map((finding) => ({
|
|
1171
|
+
package: finding.package,
|
|
1172
|
+
id: finding.id,
|
|
1173
|
+
severity: finding.severity,
|
|
1174
|
+
tier: finding.tier,
|
|
1175
|
+
reason: [
|
|
1176
|
+
finding.fix === 'none' ? 'no compatible upstream fix' : `the fix is a major upgrade (${finding.fix})`,
|
|
1177
|
+
finding.patches.length > 0
|
|
1178
|
+
? `a Socket patch exists (${finding.patches.join(', ')}) but the manifest carries another patch for this package version`
|
|
1179
|
+
: 'no Socket patch',
|
|
1180
|
+
].join('; '),
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Assembles the audit report: the shared Socket scan, then the dependency security domain and
|
|
1186
|
+
* the source code risk domain; see {@link SocketSecurityService.DOMAINS}.
|
|
1187
|
+
* @param {object} params
|
|
1188
|
+
* @param {string} [params.root]
|
|
1189
|
+
* @param {object} [params.packageJson]
|
|
1190
|
+
* @param {string[]} [params.ownPackages] - `ownPackageNamesFactory` result.
|
|
1191
|
+
* @param {object} params.dependencies - `auditRuntimeDependencies` result.
|
|
1192
|
+
* @param {object} params.npmAudit - `npmAudit` summary.
|
|
1193
|
+
* @param {object|null} [params.scan] - `scan` result.
|
|
1194
|
+
* @param {object|null} [params.packageAlerts] - `packageAlerts` result.
|
|
1195
|
+
* @param {object} params.patchScan - `patchScan` result.
|
|
1196
|
+
* @param {object} params.manifest - `patchManifestFactory` result.
|
|
1197
|
+
* @param {object} [params.remediation] - Actions this run took, by action name.
|
|
1198
|
+
* @param {object} [params.env] - Resolved Socket configuration.
|
|
1199
|
+
* @param {string} [params.cliVersion]
|
|
1200
|
+
* @returns {object}
|
|
1201
|
+
*/
|
|
1202
|
+
static securityReportFactory({
|
|
1203
|
+
root = process.cwd(),
|
|
1204
|
+
packageJson = fs.readJsonSync(nodePath.join(root, 'package.json')),
|
|
1205
|
+
ownPackages = SocketSecurityService.ownPackageNamesFactory({ packageJson }),
|
|
1206
|
+
dependencies,
|
|
1207
|
+
npmAudit,
|
|
1208
|
+
scan = null,
|
|
1209
|
+
packageAlerts = null,
|
|
1210
|
+
patchScan,
|
|
1211
|
+
manifest,
|
|
1212
|
+
remediation = {},
|
|
1213
|
+
env = {},
|
|
1214
|
+
cliVersion = '',
|
|
1215
|
+
}) {
|
|
1216
|
+
const { categoryCountsFactory, severityCountsFactory } = SocketSecurityService;
|
|
1217
|
+
const alerts = SocketSecurityService.alertFindingsFactory({
|
|
1218
|
+
artifacts: [...(scan?.artifacts ?? []), ...(packageAlerts?.artifacts ?? [])],
|
|
1219
|
+
policies: scan?.summary.alerts,
|
|
1220
|
+
ownPackages,
|
|
1221
|
+
root,
|
|
1222
|
+
version: packageJson.version,
|
|
1223
|
+
});
|
|
1224
|
+
const findings = SocketSecurityService.findingsFactory({
|
|
1225
|
+
npmAudit,
|
|
1226
|
+
patchScan: patchScan.summary,
|
|
1227
|
+
alerts: alerts.dependency,
|
|
1228
|
+
reachability: scan?.reachability,
|
|
1229
|
+
manifest,
|
|
1230
|
+
});
|
|
1231
|
+
const skipped = env.token ? 'not requested' : `${SocketSecurityService.ENV.token} is not set`;
|
|
1232
|
+
return {
|
|
1233
|
+
generatedAt: new Date().toISOString(),
|
|
1234
|
+
package: { name: packageJson.name, version: packageJson.version },
|
|
1235
|
+
socket: {
|
|
1236
|
+
cli: cliVersion || 'not installed',
|
|
1237
|
+
token: Boolean(env.token),
|
|
1238
|
+
org: env.orgSlug || '',
|
|
1239
|
+
acceptRisks: Boolean(env.acceptRisks),
|
|
1240
|
+
},
|
|
1241
|
+
scan: scan
|
|
1242
|
+
? {
|
|
1243
|
+
ok: scan.ok,
|
|
1244
|
+
message: scan.message,
|
|
1245
|
+
unavailable: scan.unavailable,
|
|
1246
|
+
healthy: scan.summary.healthy,
|
|
1247
|
+
scanId: scan.summary.scanId,
|
|
1248
|
+
orgSlug: scan.summary.orgSlug,
|
|
1249
|
+
url: scan.summary.url,
|
|
1250
|
+
artifacts: scan.artifacts?.length ?? 0,
|
|
1251
|
+
}
|
|
1252
|
+
: { skipped },
|
|
1253
|
+
dependencySecurity: {
|
|
1254
|
+
segregation: dependencies,
|
|
1255
|
+
npmAudit,
|
|
1256
|
+
reachability: scan?.reachability ?? SocketSecurityService.reachabilityFactory(null),
|
|
1257
|
+
patches: {
|
|
1258
|
+
scan: {
|
|
1259
|
+
ok: patchScan.ok,
|
|
1260
|
+
message: patchScan.message,
|
|
1261
|
+
unavailable: patchScan.unavailable,
|
|
1262
|
+
...patchScan.summary,
|
|
1263
|
+
},
|
|
1264
|
+
manifest,
|
|
1265
|
+
},
|
|
1266
|
+
remediation,
|
|
1267
|
+
categories: categoryCountsFactory(findings),
|
|
1268
|
+
counts: severityCountsFactory(findings),
|
|
1269
|
+
findings,
|
|
1270
|
+
deferred: SocketSecurityService.deferredFactory(findings),
|
|
1271
|
+
},
|
|
1272
|
+
sourceCodeRisk: {
|
|
1273
|
+
status: !packageAlerts
|
|
1274
|
+
? `skipped: ${skipped}`
|
|
1275
|
+
: packageAlerts.packages.length === 0
|
|
1276
|
+
? 'unavailable: no own package is published to the registry'
|
|
1277
|
+
: packageAlerts.artifacts
|
|
1278
|
+
? 'analyzed'
|
|
1279
|
+
: `unavailable: ${packageAlerts.message}`,
|
|
1280
|
+
packages: ownPackages,
|
|
1281
|
+
analyzed: (packageAlerts?.packages ?? []).map(({ name, version, current }) => ({ name, version, current })),
|
|
1282
|
+
categories: categoryCountsFactory(alerts.source),
|
|
1283
|
+
counts: severityCountsFactory(alerts.source),
|
|
1284
|
+
tasks: SocketSecurityService.sourceTasksFactory(alerts.source),
|
|
1285
|
+
findings: alerts.source,
|
|
1286
|
+
},
|
|
1287
|
+
counts: severityCountsFactory([...findings, ...alerts.source]),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Renders a Markdown table.
|
|
1293
|
+
* @param {string[]} headers
|
|
1294
|
+
* @param {Array<Array<*>>} rows
|
|
1295
|
+
* @returns {string}
|
|
1296
|
+
*/
|
|
1297
|
+
static markdownTable(headers, rows) {
|
|
1298
|
+
const cell = (value) => `${value ?? ''}`.replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
|
|
1299
|
+
return [
|
|
1300
|
+
`| ${headers.join(' | ')} |`,
|
|
1301
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
1302
|
+
...rows.map((row) => `| ${row.map(cell).join(' | ')} |`),
|
|
1303
|
+
].join('\n');
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Renders the report as Markdown.
|
|
1308
|
+
* @param {object} report - `securityReportFactory` result.
|
|
1309
|
+
* @returns {string}
|
|
1310
|
+
*/
|
|
1311
|
+
static securityReportMarkdownFactory(report) {
|
|
1312
|
+
const { ALERT_CATEGORIES, DOMAINS, REACHABILITY_TIERS, SEVERITIES, markdownTable: table } = SocketSecurityService;
|
|
1313
|
+
const { dependencySecurity: dependency, sourceCodeRisk: source } = report;
|
|
1314
|
+
const yesNo = (value) => (value ? 'yes' : 'no');
|
|
1315
|
+
const reachable = (value) => (value === true ? 'reachable' : value === false ? 'unreachable' : 'n/a');
|
|
1316
|
+
const label = (category) => ALERT_CATEGORIES[category]?.label ?? category;
|
|
1317
|
+
const severityRow = (name, counts) => [name, ...SEVERITIES.map((severity) => counts[severity]), counts.total];
|
|
1318
|
+
const categories = (counts) =>
|
|
1319
|
+
Object.keys(counts).length > 0
|
|
1320
|
+
? [
|
|
1321
|
+
table(
|
|
1322
|
+
['Category', 'Findings'],
|
|
1323
|
+
Object.entries(counts).map(([category, count]) => [label(category), count]),
|
|
1324
|
+
),
|
|
1325
|
+
'',
|
|
1326
|
+
]
|
|
1327
|
+
: [];
|
|
1328
|
+
const remediationRows = Object.entries(dependency.remediation).flatMap(([action, result]) =>
|
|
1329
|
+
(Array.isArray(result) ? (result.length > 0 ? result : [{ status: 'nothing to do' }]) : [result]).map((entry) => [
|
|
1330
|
+
action,
|
|
1331
|
+
entry.purl ?? entry.uuid ?? '',
|
|
1332
|
+
entry.skipped
|
|
1333
|
+
? `skipped: ${entry.skipped}`
|
|
1334
|
+
: entry.ok === false
|
|
1335
|
+
? `failed: ${entry.message}`
|
|
1336
|
+
: (entry.status ?? 'ok'),
|
|
1337
|
+
[
|
|
1338
|
+
entry.patchesApplied !== undefined && `applied ${entry.patchesApplied}`,
|
|
1339
|
+
entry.alreadyPatched !== undefined && `already patched ${entry.alreadyPatched}`,
|
|
1340
|
+
entry.failed ? `failed ${entry.failed}` : '',
|
|
1341
|
+
entry.code !== undefined && `exit ${entry.code}`,
|
|
1342
|
+
]
|
|
1343
|
+
.filter(Boolean)
|
|
1344
|
+
.join(', '),
|
|
1345
|
+
]),
|
|
1346
|
+
);
|
|
1347
|
+
const sections = [
|
|
1348
|
+
`# Security audit: ${report.package.name} ${report.package.version}`,
|
|
1349
|
+
'',
|
|
1350
|
+
`Generated ${report.generatedAt}.`,
|
|
1351
|
+
'',
|
|
1352
|
+
'## Summary',
|
|
1353
|
+
'',
|
|
1354
|
+
table(
|
|
1355
|
+
['Domain', ...SEVERITIES, 'Total'],
|
|
1356
|
+
[
|
|
1357
|
+
severityRow('Dependency security', dependency.counts),
|
|
1358
|
+
severityRow('Source code risk', source.counts),
|
|
1359
|
+
severityRow('Total', report.counts),
|
|
1360
|
+
],
|
|
1361
|
+
),
|
|
1362
|
+
'',
|
|
1363
|
+
table(
|
|
1364
|
+
['Setting', 'Value'],
|
|
1365
|
+
[
|
|
1366
|
+
['Socket CLI', report.socket.cli],
|
|
1367
|
+
[SocketSecurityService.ENV.token, yesNo(report.socket.token)],
|
|
1368
|
+
[SocketSecurityService.ENV.orgSlug, report.socket.org || 'not set'],
|
|
1369
|
+
[SocketSecurityService.ENV.acceptRisks, yesNo(report.socket.acceptRisks)],
|
|
1370
|
+
],
|
|
1371
|
+
),
|
|
1372
|
+
'',
|
|
1373
|
+
'## Socket scan',
|
|
1374
|
+
'',
|
|
1375
|
+
report.scan.skipped
|
|
1376
|
+
? `Skipped: ${report.scan.skipped}.`
|
|
1377
|
+
: table(
|
|
1378
|
+
['Field', 'Value'],
|
|
1379
|
+
[
|
|
1380
|
+
['Healthy', report.scan.healthy === null ? 'unknown' : yesNo(report.scan.healthy)],
|
|
1381
|
+
['Scan', report.scan.url || report.scan.scanId || 'none'],
|
|
1382
|
+
['Artifacts', report.scan.artifacts],
|
|
1383
|
+
['Message', report.scan.message || 'none'],
|
|
1384
|
+
],
|
|
1385
|
+
),
|
|
1386
|
+
'',
|
|
1387
|
+
'## Dependency Security',
|
|
1388
|
+
'',
|
|
1389
|
+
DOMAINS.dependency,
|
|
1390
|
+
'',
|
|
1391
|
+
'### Findings',
|
|
1392
|
+
'',
|
|
1393
|
+
...categories(dependency.categories),
|
|
1394
|
+
dependency.findings.length > 0
|
|
1395
|
+
? table(
|
|
1396
|
+
['Severity', 'Category', 'Package', 'Advisory or alert', 'Tier', 'Reachability', 'Fix', 'Patch', 'Sources'],
|
|
1397
|
+
dependency.findings.map((finding) => [
|
|
1398
|
+
finding.severity,
|
|
1399
|
+
label(finding.category),
|
|
1400
|
+
`${finding.package}${finding.installed ? `@${finding.installed}` : ''}`,
|
|
1401
|
+
finding.id,
|
|
1402
|
+
finding.tier,
|
|
1403
|
+
reachable(finding.reachable),
|
|
1404
|
+
finding.fix,
|
|
1405
|
+
finding.patched
|
|
1406
|
+
? 'applied'
|
|
1407
|
+
: finding.patches.length > 0
|
|
1408
|
+
? `available ${finding.patches.join(' ')}`
|
|
1409
|
+
: 'none',
|
|
1410
|
+
finding.sources.join(', '),
|
|
1411
|
+
]),
|
|
1412
|
+
)
|
|
1413
|
+
: 'No findings.',
|
|
1414
|
+
'',
|
|
1415
|
+
'### Segregation',
|
|
1416
|
+
'',
|
|
1417
|
+
table(
|
|
1418
|
+
['Group', 'Count'],
|
|
1419
|
+
[
|
|
1420
|
+
['dependencies', dependency.segregation.dependencies],
|
|
1421
|
+
['devDependencies', dependency.segregation.devDependencies],
|
|
1422
|
+
['packages the runtime graph imports', dependency.segregation.runtime.length],
|
|
1423
|
+
],
|
|
1424
|
+
),
|
|
1425
|
+
'',
|
|
1426
|
+
dependency.segregation.misplaced.length > 0
|
|
1427
|
+
? table(
|
|
1428
|
+
['Runtime import outside dependencies', 'Declared in', 'Importers'],
|
|
1429
|
+
dependency.segregation.misplaced.map(({ name, declaredIn, importers }) => [
|
|
1430
|
+
name,
|
|
1431
|
+
declaredIn,
|
|
1432
|
+
importers.join(', '),
|
|
1433
|
+
]),
|
|
1434
|
+
)
|
|
1435
|
+
: 'Every package the runtime graph imports is declared in `dependencies`.',
|
|
1436
|
+
'',
|
|
1437
|
+
`Lazy imports: ${dependency.segregation.lazy.join(', ') || 'none'}.`,
|
|
1438
|
+
'',
|
|
1439
|
+
'### npm audit',
|
|
1440
|
+
'',
|
|
1441
|
+
table(
|
|
1442
|
+
[...SEVERITIES, 'Total'],
|
|
1443
|
+
[[...SEVERITIES.map((severity) => dependency.npmAudit.counts[severity]), dependency.npmAudit.counts.total]],
|
|
1444
|
+
),
|
|
1445
|
+
'',
|
|
1446
|
+
dependency.npmAudit.findings.length > 0
|
|
1447
|
+
? table(
|
|
1448
|
+
['Package', 'Severity', 'Range', 'Direct', 'Fix', 'Advisories'],
|
|
1449
|
+
dependency.npmAudit.findings.map((finding) => [
|
|
1450
|
+
finding.name,
|
|
1451
|
+
finding.severity,
|
|
1452
|
+
finding.range,
|
|
1453
|
+
yesNo(finding.direct),
|
|
1454
|
+
finding.fix,
|
|
1455
|
+
finding.advisories.map((advisory) => advisory.url).join(' '),
|
|
1456
|
+
]),
|
|
1457
|
+
)
|
|
1458
|
+
: 'No known vulnerabilities in the lockfile.',
|
|
1459
|
+
'',
|
|
1460
|
+
'### Reachability',
|
|
1461
|
+
'',
|
|
1462
|
+
table(
|
|
1463
|
+
['Tier', 'Meaning'],
|
|
1464
|
+
Object.entries(REACHABILITY_TIERS).map(([tier, meaning]) => [tier, meaning]),
|
|
1465
|
+
),
|
|
1466
|
+
'',
|
|
1467
|
+
dependency.reachability.available
|
|
1468
|
+
? table(
|
|
1469
|
+
['Package', 'Version', 'Advisory', 'Result'],
|
|
1470
|
+
dependency.reachability.results.map((result) => [
|
|
1471
|
+
result.package,
|
|
1472
|
+
result.version,
|
|
1473
|
+
result.ghsaId,
|
|
1474
|
+
reachable(result.reachable),
|
|
1475
|
+
]),
|
|
1476
|
+
)
|
|
1477
|
+
: 'Tier 1 analysis not run: `underpost socketsecurity --audit --reach` needs a Socket API token.',
|
|
1478
|
+
'',
|
|
1479
|
+
'### Socket patches',
|
|
1480
|
+
'',
|
|
1481
|
+
table(
|
|
1482
|
+
['Field', 'Value'],
|
|
1483
|
+
[
|
|
1484
|
+
['Scanned packages', dependency.patches.scan.scannedPackages],
|
|
1485
|
+
['Packages with patches', dependency.patches.scan.packagesWithPatches],
|
|
1486
|
+
['Free patches', dependency.patches.scan.freePatches],
|
|
1487
|
+
['Paid patches', dependency.patches.scan.paidPatches],
|
|
1488
|
+
['Paid tier access', yesNo(dependency.patches.scan.canAccessPaidPatches)],
|
|
1489
|
+
[
|
|
1490
|
+
'Status',
|
|
1491
|
+
dependency.patches.scan.message || (dependency.patches.scan.unavailable ? 'CLI not installed' : 'ok'),
|
|
1492
|
+
],
|
|
1493
|
+
],
|
|
1494
|
+
),
|
|
1495
|
+
'',
|
|
1496
|
+
dependency.patches.scan.available.length > 0
|
|
1497
|
+
? table(
|
|
1498
|
+
['Severity', 'PURL', 'UUID', 'Tier', 'Title'],
|
|
1499
|
+
dependency.patches.scan.available.map((patch) => [
|
|
1500
|
+
patch.severity,
|
|
1501
|
+
patch.purl,
|
|
1502
|
+
patch.uuid,
|
|
1503
|
+
patch.tier,
|
|
1504
|
+
patch.title,
|
|
1505
|
+
]),
|
|
1506
|
+
)
|
|
1507
|
+
: 'No patch available for the installed versions.',
|
|
1508
|
+
'',
|
|
1509
|
+
`Manifest \`${dependency.patches.manifest.path}\`: ${dependency.patches.manifest.exists ? `${dependency.patches.manifest.patches.length} patch(es)` : 'absent'}.`,
|
|
1510
|
+
'',
|
|
1511
|
+
...(dependency.patches.manifest.patches.length > 0
|
|
1512
|
+
? [
|
|
1513
|
+
table(
|
|
1514
|
+
['PURL', 'UUID', 'Tier', 'Vulnerabilities', 'Files'],
|
|
1515
|
+
dependency.patches.manifest.patches.map((patch) => [
|
|
1516
|
+
patch.purl,
|
|
1517
|
+
patch.uuid,
|
|
1518
|
+
patch.tier,
|
|
1519
|
+
patch.vulnerabilities
|
|
1520
|
+
.map((vulnerability) => `${vulnerability.id} (${vulnerability.severity})`)
|
|
1521
|
+
.join(' '),
|
|
1522
|
+
patch.files.length,
|
|
1523
|
+
]),
|
|
1524
|
+
),
|
|
1525
|
+
'',
|
|
1526
|
+
]
|
|
1527
|
+
: []),
|
|
1528
|
+
'### Remediation',
|
|
1529
|
+
'',
|
|
1530
|
+
remediationRows.length > 0
|
|
1531
|
+
? table(['Action', 'Target', 'Result', 'Details'], remediationRows)
|
|
1532
|
+
: 'No remediation ran in this pass.',
|
|
1533
|
+
'',
|
|
1534
|
+
'### Deferred risks',
|
|
1535
|
+
'',
|
|
1536
|
+
dependency.deferred.length > 0
|
|
1537
|
+
? table(
|
|
1538
|
+
['Severity', 'Package', 'Advisory or alert', 'Tier', 'Reason'],
|
|
1539
|
+
dependency.deferred.map((entry) => [entry.severity, entry.package, entry.id, entry.tier, entry.reason]),
|
|
1540
|
+
)
|
|
1541
|
+
: 'None.',
|
|
1542
|
+
'',
|
|
1543
|
+
'## Source Code Risk Analysis',
|
|
1544
|
+
'',
|
|
1545
|
+
DOMAINS.source,
|
|
1546
|
+
'',
|
|
1547
|
+
table(
|
|
1548
|
+
['Field', 'Value'],
|
|
1549
|
+
[
|
|
1550
|
+
['Status', source.status],
|
|
1551
|
+
['Own packages', source.packages.join(', ') || 'none'],
|
|
1552
|
+
[
|
|
1553
|
+
'Analyzed',
|
|
1554
|
+
source.analyzed
|
|
1555
|
+
.map(
|
|
1556
|
+
({ name, version, current }) => `${name}@${version}${current ? '' : ' (not the checked-out version)'}`,
|
|
1557
|
+
)
|
|
1558
|
+
.join(', ') || 'none',
|
|
1559
|
+
],
|
|
1560
|
+
],
|
|
1561
|
+
),
|
|
1562
|
+
'',
|
|
1563
|
+
'### Refactoring tasks',
|
|
1564
|
+
'',
|
|
1565
|
+
source.tasks.length > 0
|
|
1566
|
+
? table(
|
|
1567
|
+
['Severity', 'Category', 'Task', 'Alerts', 'Locations'],
|
|
1568
|
+
source.tasks.map((task) => [
|
|
1569
|
+
task.severity,
|
|
1570
|
+
task.label,
|
|
1571
|
+
task.task,
|
|
1572
|
+
task.types.join(', '),
|
|
1573
|
+
task.locations.join(', '),
|
|
1574
|
+
]),
|
|
1575
|
+
)
|
|
1576
|
+
: 'No source code alerts.',
|
|
1577
|
+
'',
|
|
1578
|
+
'### Findings',
|
|
1579
|
+
'',
|
|
1580
|
+
...categories(source.categories),
|
|
1581
|
+
source.findings.length > 0
|
|
1582
|
+
? table(
|
|
1583
|
+
['Severity', 'Category', 'Alert', 'Location', 'Package', 'Policy'],
|
|
1584
|
+
source.findings.map((finding) => [
|
|
1585
|
+
finding.severity,
|
|
1586
|
+
label(finding.category),
|
|
1587
|
+
finding.type,
|
|
1588
|
+
SocketSecurityService.locationLabelFactory(finding),
|
|
1589
|
+
`${finding.package}@${finding.installed}`,
|
|
1590
|
+
finding.policy || 'none',
|
|
1591
|
+
]),
|
|
1592
|
+
)
|
|
1593
|
+
: 'No findings.',
|
|
1594
|
+
'',
|
|
1595
|
+
];
|
|
1596
|
+
return sections.join('\n');
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Writes the report and the raw documents behind it.
|
|
1601
|
+
* @param {object} params
|
|
1602
|
+
* @param {object} params.report - `securityReportFactory` result.
|
|
1603
|
+
* @param {Record<string, object|null>} [params.artifacts] - Raw documents by `REPORT_FILES` key.
|
|
1604
|
+
* @param {string} [params.directory] - Output directory.
|
|
1605
|
+
* @returns {string[]} Files written.
|
|
1606
|
+
*/
|
|
1607
|
+
static writeSecurityReports({ report, artifacts = {}, directory = SocketSecurityService.REPORT_DIRECTORY }) {
|
|
1608
|
+
const { REPORT_FILES } = SocketSecurityService;
|
|
1609
|
+
fs.ensureDirSync(directory);
|
|
1610
|
+
const files = [];
|
|
1611
|
+
const write = (file, content) => {
|
|
1612
|
+
const path = nodePath.join(directory, file);
|
|
1613
|
+
fs.writeFileSync(path, content, 'utf8');
|
|
1614
|
+
files.push(path);
|
|
1615
|
+
};
|
|
1616
|
+
write(REPORT_FILES.report, `${JSON.stringify(report, null, 2)}\n`);
|
|
1617
|
+
write(REPORT_FILES.markdown, SocketSecurityService.securityReportMarkdownFactory(report));
|
|
1618
|
+
for (const [key, document] of Object.entries(artifacts))
|
|
1619
|
+
if (document !== null && document !== undefined && REPORT_FILES[key])
|
|
1620
|
+
write(REPORT_FILES[key], `${JSON.stringify(document, null, 2)}\n`);
|
|
1621
|
+
logger.info('Security reports written', { directory, files: files.map((file) => nodePath.basename(file)) });
|
|
1622
|
+
return files;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Whether the Socket scan failed or broke the policy, or findings at or above a severity remain
|
|
1627
|
+
* in either domain.
|
|
1628
|
+
* @param {object} report - `securityReportFactory` result.
|
|
1629
|
+
* @param {string} [threshold]
|
|
1630
|
+
* @returns {boolean}
|
|
1631
|
+
*/
|
|
1632
|
+
static reportViolates(report, threshold = 'high') {
|
|
1633
|
+
const limit = SocketSecurityService.severityRank(threshold);
|
|
1634
|
+
const findings = [...(report.dependencySecurity?.findings ?? []), ...(report.sourceCodeRisk?.findings ?? [])];
|
|
1635
|
+
return (
|
|
1636
|
+
report.scan?.ok === false ||
|
|
1637
|
+
report.scan?.healthy === false ||
|
|
1638
|
+
findings.some((finding) => SocketSecurityService.severityRank(finding.severity) <= limit)
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const {
|
|
1644
|
+
SEVERITIES,
|
|
1645
|
+
alertCategoryFactory,
|
|
1646
|
+
alertFindingsFactory,
|
|
1647
|
+
artifactNameFactory,
|
|
1648
|
+
categoryCountsFactory,
|
|
1649
|
+
childEnvFactory,
|
|
1650
|
+
deferredFactory,
|
|
1651
|
+
findingsFactory,
|
|
1652
|
+
fix,
|
|
1653
|
+
ghsaIdFactory,
|
|
1654
|
+
locationLabelFactory,
|
|
1655
|
+
npmAudit,
|
|
1656
|
+
npmAuditFix,
|
|
1657
|
+
npmAuditSummaryFactory,
|
|
1658
|
+
orgArgsFactory,
|
|
1659
|
+
ownPackageNamesFactory,
|
|
1660
|
+
packageAlerts,
|
|
1661
|
+
parseJsonOutput,
|
|
1662
|
+
patchApply,
|
|
1663
|
+
patchGet,
|
|
1664
|
+
patchManifestFactory,
|
|
1665
|
+
patchScan,
|
|
1666
|
+
patchScanSummaryFactory,
|
|
1667
|
+
patchSelectionFactory,
|
|
1668
|
+
patchSetup,
|
|
1669
|
+
publishedPackagesFactory,
|
|
1670
|
+
purlPackageFactory,
|
|
1671
|
+
reachabilityFactory,
|
|
1672
|
+
reportViolates,
|
|
1673
|
+
runSocket,
|
|
1674
|
+
scan,
|
|
1675
|
+
scanReportSummaryFactory,
|
|
1676
|
+
securityReportFactory,
|
|
1677
|
+
securityReportMarkdownFactory,
|
|
1678
|
+
severityCountsFactory,
|
|
1679
|
+
socketBinaryFactory,
|
|
1680
|
+
socketCommandFactory,
|
|
1681
|
+
socketEnvFactory,
|
|
1682
|
+
socketResultFactory,
|
|
1683
|
+
socketVersion,
|
|
1684
|
+
sourceLocationFactory,
|
|
1685
|
+
sourceTasksFactory,
|
|
1686
|
+
writeSecurityReports,
|
|
1687
|
+
} = SocketSecurityService;
|
|
1688
|
+
|
|
1689
|
+
export default SocketSecurityService;
|
|
1690
|
+
|
|
1691
|
+
export {
|
|
1692
|
+
SEVERITIES,
|
|
1693
|
+
alertCategoryFactory,
|
|
1694
|
+
alertFindingsFactory,
|
|
1695
|
+
artifactNameFactory,
|
|
1696
|
+
categoryCountsFactory,
|
|
1697
|
+
childEnvFactory,
|
|
1698
|
+
deferredFactory,
|
|
1699
|
+
findingsFactory,
|
|
1700
|
+
fix,
|
|
1701
|
+
ghsaIdFactory,
|
|
1702
|
+
locationLabelFactory,
|
|
1703
|
+
npmAudit,
|
|
1704
|
+
npmAuditFix,
|
|
1705
|
+
npmAuditSummaryFactory,
|
|
1706
|
+
orgArgsFactory,
|
|
1707
|
+
ownPackageNamesFactory,
|
|
1708
|
+
packageAlerts,
|
|
1709
|
+
parseJsonOutput,
|
|
1710
|
+
patchApply,
|
|
1711
|
+
patchGet,
|
|
1712
|
+
patchManifestFactory,
|
|
1713
|
+
patchScan,
|
|
1714
|
+
patchScanSummaryFactory,
|
|
1715
|
+
patchSelectionFactory,
|
|
1716
|
+
patchSetup,
|
|
1717
|
+
publishedPackagesFactory,
|
|
1718
|
+
purlPackageFactory,
|
|
1719
|
+
reachabilityFactory,
|
|
1720
|
+
reportViolates,
|
|
1721
|
+
runSocket,
|
|
1722
|
+
scan,
|
|
1723
|
+
scanReportSummaryFactory,
|
|
1724
|
+
securityReportFactory,
|
|
1725
|
+
securityReportMarkdownFactory,
|
|
1726
|
+
severityCountsFactory,
|
|
1727
|
+
socketBinaryFactory,
|
|
1728
|
+
socketCommandFactory,
|
|
1729
|
+
socketEnvFactory,
|
|
1730
|
+
socketResultFactory,
|
|
1731
|
+
socketVersion,
|
|
1732
|
+
sourceLocationFactory,
|
|
1733
|
+
sourceTasksFactory,
|
|
1734
|
+
writeSecurityReports,
|
|
1735
|
+
};
|