Skip to content

Commit

Permalink
refactor(vuln): adding multiple strategies to hydrate Node Secure's p…
Browse files Browse the repository at this point in the history
…ayload vulnerabilities using npm's audit strategy. (#75)

* feat: adding npm audit to the vulnerabilities sources strategies.

* fix: adding npm audit as a vulnerabilities source strategy

* fix: remove unused callback.

* sync w/ master.

* fix: update package-lock after arborist@^2.2.6 upgrade.

* fix: null coalescing operator uncompatibility with Node < 14

* refacto: removing explicit declarations and adding more abstraction.

* moving side effects (specific logic linked to a specific vuln mode) in their respective scope.

* refactor: moving strategies initialization to main entry file to allow vuln strategies to be used by API + CLI. Add vuln strategy type to nsecure payload (+ typings) which is not used for now.

* fix: cleaning security-wg test
  • Loading branch information
antoine-coulon committed Mar 20, 2021
1 parent cb38008 commit 236c733
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 75 deletions.
28 changes: 11 additions & 17 deletions bin/index.js
Expand Up @@ -24,19 +24,19 @@ const ui = require("cliui")();
// Require Internal Dependencies
const startHTTPServer = require("../src/httpServer.js");
const i18n = require("../src/i18n");
const { getRegistryURL, loadNsecureCache, writeNsecureCache, formatBytes } = require("../src/utils");
const { getRegistryURL, formatBytes } = require("../src/utils");
const { depWalker } = require("../src/depWalker");
const { hydrateDB, deleteDB } = require("../src/vulnerabilities");
const { cwd, verify } = require("../index");


// CONSTANTS
const REGISTRY_DEFAULT_ADDR = getRegistryURL();
const LOCAL_CACHE = loadNsecureCache();
const ONE_DAY = 3600000 * 24;
const token = typeof process.env.NODE_SECURE_TOKEN === "string" ? { token: process.env.NODE_SECURE_TOKEN } : {};

// Process script arguments
const version = require("../package.json").version;
const { setVulnerabilityStrategy } = require("../src/vulnerabilities/vulnerabilitySource.js");
const { VULN_MODE_DB_SECURITY_WG } = require("../src/vulnerabilities/strategies.js");
const prog = sade("nsecure").version(version);
console.log(grey().bold(`\n > ${i18n.getToken("cli.executing_at")}: ${yellow().bold(process.cwd())}\n`));

Expand All @@ -61,15 +61,6 @@ function logAndWrite(payload, output = "nsecure-result") {
return filePath;
}

async function checkHydrateDB() {
const ts = Math.abs(Date.now() - LOCAL_CACHE.lastUpdated);

if (ts > ONE_DAY) {
await hydrateCmd();
writeNsecureCache();
}
}

prog
.command("hydrate-db")
.describe(i18n.getToken("cli.commands.hydrate_db.desc"))
Expand All @@ -82,6 +73,7 @@ prog
.option("-o, --output", i18n.getToken("cli.commands.option_output"), "nsecure-result")
.option("-n, --nolock", i18n.getToken("cli.commands.cwd.option_nolock"), false)
.option("-f, --full", i18n.getToken("cli.commands.cwd.option_full"), false)
.option("-s, --vulnerabilityStrategy", i18n.getToken("cli.commands.strategy"), VULN_MODE_DB_SECURITY_WG)
.action(cwdCmd);

prog
Expand Down Expand Up @@ -379,6 +371,8 @@ async function verifyCmd(packageName = null, options) {
}

async function hydrateCmd() {
const { deleteDB, hydrateDB } = await setVulnerabilityStrategy(VULN_MODE_DB_SECURITY_WG, { sideEffects: false });

deleteDB();

const spinner = new Spinner({
Expand Down Expand Up @@ -423,10 +417,11 @@ async function autoCmd(packageName, opts) {
}

async function cwdCmd(opts) {
const { depth: maxDepth = 4, output, nolock, full } = opts;
const { depth: maxDepth = 4, output, nolock, full, vulnerabilityStrategy } = opts;

await checkHydrateDB();
const payload = await cwd(void 0, { verbose: true, maxDepth, usePackageLock: !nolock, fullLockMode: full });
const payload = await cwd(void 0,
{ verbose: true, maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy }
);

return logAndWrite(payload, output);
}
Expand All @@ -435,7 +430,6 @@ async function fromCmd(packageName, opts) {
const { depth: maxDepth = 4, output } = opts;
let manifest = null;

await checkHydrateDB();
const spinner = new Spinner({
text: white().bold(i18n.getToken("cli.commands.from.searching", yellow().bold(packageName)))
}).start();
Expand Down
1 change: 1 addition & 0 deletions i18n/english.js
Expand Up @@ -15,6 +15,7 @@ module.exports = {
commands: {
option_depth: "Maximum dependencies depth to fetch",
option_output: "Json file output name",
strategy: "Vulnerabilities source to use",
hydrate_db: {
desc: "Hydrate the vulnerabilities db",
running: tS`Hydrating local vulnerabilities with the '${0}' database...`,
Expand Down
1 change: 1 addition & 0 deletions i18n/french.js
Expand Up @@ -15,6 +15,7 @@ module.exports = {
commands: {
option_depth: "Niveau de profondeur de dépendances maximum à aller chercher",
option_output: "Nom de sortie du fichier json",
strategy: "Source de vulnérabilités à utiliser",
hydrate_db: {
desc: "Mise à jour de la base de vulnérabilité",
running: tS`Mise à jour locale des vulnérabilités avec la base '${0}'...`,
Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Expand Up @@ -74,6 +74,8 @@ declare namespace NodeSecure {
coordinating_vendor: string;
}

type VulnerabilityStrategy = "db_npm" | "db_security_wg";

interface VersionDescriptor {
metadata: {
dependencyCount: number;
Expand Down Expand Up @@ -119,6 +121,7 @@ declare namespace NodeSecure {
warnings: [];
dependencies: Record<string, VersionDescriptor>;
version: string;
vulnerabilityStrategy: VulnerabilityStrategy;
}

interface VerifyPayload {
Expand All @@ -140,6 +143,7 @@ declare namespace NodeSecure {
readonly verbose?: boolean;
readonly maxDepth?: number;
readonly usePackageLock?: boolean;
readonly vulnerabilityStrategy: VulnerabilityStrategy;
}

export function cwd(path: string, options?: NodeSecure.Options): Promise<NodeSecure.Payload>;
Expand Down
8 changes: 7 additions & 1 deletion index.js
Expand Up @@ -15,6 +15,8 @@ const isMinified = require("is-minified-code");
// Require Internal Dependencies
const { depWalker } = require("./src/depWalker");
const { getRegistryURL, getTarballComposition, recursiveRmdir } = require("./src/utils");
const { setVulnerabilityStrategy } = require("./src/vulnerabilities/vulnerabilitySource.js");
const { VULN_MODE_DB_SECURITY_WG } = require("./src/vulnerabilities/strategies.js");

// CONSTANTS
const TMP = os.tmpdir();
Expand All @@ -28,17 +30,21 @@ async function cwd(cwd = process.cwd(), options) {
const packagePath = join(cwd, "package.json");
const str = await readFile(packagePath, "utf-8");
options.forceRootAnalysis = true;
if (!Reflect.has(options, "usePackageLock")) {
if (!("usePackageLock" in options)) {
options.usePackageLock = true;
}

setVulnerabilityStrategy("vulnerabilityStrategy" in options ? options.vulnerabilityStrategy : VULN_MODE_DB_SECURITY_WG);

return depWalker(JSON.parse(str), options);
}

async function from(packageName, options) {
const token = typeof process.env.NODE_SECURE_TOKEN === "string" ? { token: process.env.NODE_SECURE_TOKEN } : {};
const manifest = await pacote.manifest(packageName, token);

setVulnerabilityStrategy("vulnerabilityStrategy" in options ? options.vulnerabilityStrategy : VULN_MODE_DB_SECURITY_WG);

return depWalker(manifest, options);
}

Expand Down
21 changes: 16 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -86,7 +86,7 @@
"vis-network": "^9.0.2"
},
"dependencies": {
"@npmcli/arborist": "^2.2.3",
"@npmcli/arborist": "^2.2.6",
"@polka/send-type": "^0.5.2",
"@slimio/async-cli-spinner": "^0.5.2",
"@slimio/github": "^0.5.0",
Expand Down
11 changes: 8 additions & 3 deletions src/depWalker.js
Expand Up @@ -20,7 +20,7 @@ const is = require("@slimio/is");

// Require Internal Dependencies
const { mergeDependencies, cleanRange, recursiveRmdir, constants } = require("./utils");
const { hydrateNodeSecurePayload } = require("./vulnerabilities");
const { getVulnerabilityStrategy } = require("./vulnerabilities/vulnerabilitySource");
const { analyzeDirOrArchiveOnDisk } = require("./tarball");
const Dependency = require("./dependency.class");
const applyWarnings = require("./warnings");
Expand Down Expand Up @@ -244,6 +244,7 @@ async function depWalker(manifest, options = Object.create(null)) {

// Create TMP directory
const tmpLocation = await mkdtemp(join(os.tmpdir(), "/"));

const id = tmpLocation.slice(-6);

const payload = {
Expand Down Expand Up @@ -331,8 +332,12 @@ async function depWalker(manifest, options = Object.create(null)) {
regSpinner.succeed(white().bold(i18n.getToken("depWalker.success_registry_metadata")));
}

// Search for vulnerabilities in the local .json db
await hydrateNodeSecurePayload(payload.dependencies);
// Search for vulnerabilities relatively to the current initialized strategy
const vulnStrategy = await getVulnerabilityStrategy();
await vulnStrategy.hydrateNodeSecurePayload(payload.dependencies);

payload.vulnerabilityStrategy = vulnStrategy.type;


// We do this because it "seem" impossible to link all dependencies in the first walk.
// Because we are dealing with package only one time it may happen sometimes.
Expand Down
9 changes: 9 additions & 0 deletions src/vulnerabilities/strategies.js
@@ -0,0 +1,9 @@
"use strict";

const VULN_MODE_DB_SECURITY_WG = "db_security_wg";
const VULN_MODE_NPM_AUDIT = "db_npm";

module.exports = {
VULN_MODE_DB_SECURITY_WG,
VULN_MODE_NPM_AUDIT
};
63 changes: 63 additions & 0 deletions src/vulnerabilities/strategies/npm-audit.js
@@ -0,0 +1,63 @@
/* eslint-disable class-methods-use-this */
"use strict";

// Require Third-party Dependencies
const Arborist = require("@npmcli/arborist");

// CONSTANTS
const { constants } = require("../../utils");
const { VULN_MODE_NPM_AUDIT } = require("../strategies");


function NPMAuditStrategy() {
return {
type: VULN_MODE_NPM_AUDIT,
hydrateNodeSecurePayload
};
}

async function hydrateNodeSecurePayload(dependencies) {
const arborist = new Arborist({ ...constants.NPM_TOKEN, registry: constants.DEFAULT_REGISTRY_ADDR });

try {
const { vulnerabilities } = (await arborist.audit()).toJSON();

Object.keys(vulnerabilities).forEach((packageName) => {
const packageVulnerabilities = extractPackageVulnsFromSource(vulnerabilities[packageName]);
const dependenciesVulnerabilities = dependencies.get(packageName).vulnerabilities;

dependenciesVulnerabilities.push(packageVulnerabilities);
});
}
// eslint-disable-next-line no-empty
catch {}
}

function extractPackageVulnsFromSource(packageVulnerabilities) {
const vulnerabilitiesFromSource = [];
const { via: vulnSources } = packageVulnerabilities;

for (const vulnSource of vulnSources) {
const {
title, range, id,
module_name: name,
severity, version,
vulnerableVersions
} = vulnSource;

const vulnerability = {
title,
module_name: name,
severity, version,
vulnerableVersions,
range,
id
};
vulnerabilitiesFromSource.push(vulnerability);
}

return vulnerabilitiesFromSource;
}


module.exports = { NPMAuditStrategy };

0 comments on commit 236c733

Please sign in to comment.