snyk-mvn-plugin
Version:
Snyk CLI Maven plugin
101 lines • 4.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.readM2HashLabels = readM2HashLabels;
const fs = require("fs");
const index_1 = require("../index");
/**
* Read install-time-recorded hashes from the Maven local repository.
*
* For each artifact in `~/.m2/repository/.../`, Maven stores the JAR alongside
* companion checksum files (`<artifact>.jar.sha1`, `.md5`, `.sha256`, sometimes
* `.sha512`). Those files came from the upstream Maven repository at install
* time and Maven verified them against the JAR bytes before saving. They are
* therefore the install-time-recorded hash of the artifact the customer's
* Maven received.
*
* This module reads those files (no hashing happens here) and surfaces the
* contents as depgraph labels keyed by `hash:<algorithm>`, where `<algorithm>`
* is a lowercase-normalized name (`md5`, `sha-1`, `sha-256`, `sha-512`). The
* downstream consumer maps these to the CycloneDX/SPDX algorithm names it
* requires.
*/
// Maps Maven companion-file extensions to the CycloneDX/SBOM hash-algorithm
// label suffix (the label key emitted is `hash:<algorithm>`) and the pattern a
// valid digest of that algorithm must match: exactly N lowercase hex chars.
// The patterns are compiled once here, not per file. They carry no global/
// sticky flag, so they hold no mutable `lastIndex` state and a single shared
// instance is safe to reuse across the concurrent reads below.
const M2_COMPANION_FILES = [
{ ext: 'md5', algorithm: 'md5', digestPattern: /^[0-9a-f]{32}$/ },
{ ext: 'sha1', algorithm: 'sha-1', digestPattern: /^[0-9a-f]{40}$/ },
{ ext: 'sha256', algorithm: 'sha-256', digestPattern: /^[0-9a-f]{64}$/ },
{ ext: 'sha512', algorithm: 'sha-512', digestPattern: /^[0-9a-f]{128}$/ },
];
// Upper bound on the bytes read from a companion file. We only keep the first
// whitespace-delimited token, and the longest valid digest is sha-512 at 128
// hex chars; 256 bytes leaves slack for a leading BOM/whitespace while still
// reaching that token. Reading a small prefix rather than the whole file stops
// a misconfigured mirror that stored a large HTML error page from being
// buffered wholesale — anything past the first token is irrelevant.
const MAX_COMPANION_BYTES = 256;
/**
* Read a single companion-file value. `found` is false if the file does not
* exist (older artifacts may not publish every algorithm) or is unreadable;
* otherwise true. `digest` is set only if the file was found and its contents
* are a valid digest for the algorithm.
*
* Maven companion files contain the digest as ASCII hex, sometimes followed by a
* space and the filename; we keep only the first whitespace-delimited token. The
* token is rejected unless it matches `digestPattern` (the algorithm's exact
* lowercase-hex shape) — a misconfigured mirror can store an HTML error page or
* truncated body in a companion file, and we must not surface that as a hash.
*/
async function readCompanionFile(artifactPath, ext, digestPattern) {
const companionPath = `${artifactPath}.${ext}`;
let raw;
let handle;
try {
handle = await fs.promises.open(companionPath, 'r');
const buffer = Buffer.alloc(MAX_COMPANION_BYTES);
const { bytesRead } = await handle.read(buffer, 0, MAX_COMPANION_BYTES, 0);
raw = buffer.toString('utf8', 0, bytesRead).trim();
}
catch {
// File does not exist (older artifacts may not publish every algorithm) or
// is unreadable — treat both as "no recorded hash for this algorithm".
return { found: false };
}
finally {
await handle?.close();
}
const digest = raw.split(/\s+/, 1)[0].toLowerCase();
if (!digestPattern.test(digest)) {
(0, index_1.debug)(`Ignoring ${ext} companion file ${companionPath}: ` +
`contents are not a valid digest`);
return { found: true };
}
return { found: true, digest };
}
/**
* Given the path to an artifact in the local Maven repository, read whichever
* companion checksum files exist for it and return them as
* `hash:<algorithm>` -> hex labels.
*
* Empty result if none are present (artifact not in .m2 yet, or no companion
* files published).
*/
async function readM2HashLabels(artifactPath) {
const labels = {};
const results = await Promise.all(M2_COMPANION_FILES.map(({ ext, digestPattern }) => readCompanionFile(artifactPath, ext, digestPattern)));
M2_COMPANION_FILES.forEach(({ algorithm }, i) => {
const { digest } = results[i];
if (digest) {
labels[`hash:${algorithm}`] = digest;
}
});
if (results.every((result) => !result.found)) {
(0, index_1.debug)(`No companion checksum files found for ${artifactPath}`);
}
return labels;
}
//# sourceMappingURL=m2-hash-labels.js.map