@nodesecure/scanner
Version:
A package API to run a static analysis of your module's dependencies.
149 lines • 6.29 kB
JavaScript
// Import Node.js Dependencies
import path from "node:path";
// Import Third-party Dependencies
import semver from "semver";
import * as npmRegistrySDK from "@nodesecure/npm-registry-sdk";
import { packageJSONIntegrityHash } from "@nodesecure/mama";
import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk";
import * as i18n from "@nodesecure/i18n";
import { isHTTPError } from "@openally/httpie";
// Import Internal Dependencies
import { PackumentExtractor } from "./PackumentExtractor.js";
import { fetchNpmAvatars } from "./fetchNpmAvatars.js";
import { Logger } from "../class/logger.class.js";
import { getLinks } from "../utils/getLinks.js";
import { getDirNameFromUrl } from "../utils/dirname.js";
// CONSTANTS
const kNotFoundStatusCode = 404;
await i18n.extendFromSystemPath(path.join(getDirNameFromUrl(import.meta.url), "..", "i18n"));
export class NpmRegistryProvider {
#date;
#npmApiClient;
#registry;
#tokenStore;
name;
version;
constructor(name, version, options = {}) {
const { dateProvider = undefined, npmApiClient = npmRegistrySDK, registry = npmRegistrySDK.getLocalRegistryURL(), tokenStore = undefined } = options;
this.name = name;
this.version = version;
this.#date = dateProvider;
this.#npmApiClient = npmApiClient;
this.#registry = registry;
this.#tokenStore = tokenStore;
}
async collectPackageVersionData() {
const packumentVersion = await this.#npmApiClient.packumentVersion(this.name, this.version, {
registry: this.#registry,
token: this.#tokenStore?.get(this.#registry)
});
const { integrity } = packageJSONIntegrityHash(packumentVersion, {
isFromRemoteRegistry: true
});
return {
links: getLinks(packumentVersion),
integrity,
deprecated: packumentVersion.deprecated,
signatures: packumentVersion.dist.signatures,
attestations: packumentVersion.dist.attestations
};
}
async collectPackageData() {
const packument = await this.#npmApiClient.packument(this.name, {
registry: this.#registry,
token: this.#tokenStore?.get(this.#registry)
});
const packumentVersion = packument.versions[this.version];
const metadata = new PackumentExtractor(packument, { dateProvider: this.#date }).getMetadata(this.version);
const flags = {
isOutdated: semver.neq(this.version, metadata.lastVersion),
isDeprecated: packumentVersion.deprecated
};
return {
metadata,
flags: Object.keys(flags).filter((key) => flags[key]),
version: {
links: getLinks(packumentVersion),
deprecated: packumentVersion.deprecated
}
};
}
async enrichDependency(logger, dependency) {
try {
const { metadata, flags, version } = await this.collectPackageData();
await fetchNpmAvatars(metadata);
const dependencyVersion = dependency.versions[this.version];
dependency.metadata = metadata;
dependencyVersion.flags = [...dependencyVersion.flags, ...flags];
Object.assign(dependencyVersion, version);
}
catch {
// ignored
}
finally {
logger.tick("registry");
}
}
async enrichDependencyVersion(dependency, warnings, org) {
try {
const { integrity, deprecated, links, signatures, attestations } = await this.collectPackageVersionData();
Object.assign(dependency.versions[this.version], {
links,
deprecated,
attestations
});
dependency.metadata.integrity[this.version] = integrity;
if (this.#registry === getNpmRegistryURL()) {
return;
}
try {
const packumentVersionFromPublicRegistry = await this.#npmApiClient.packumentVersion(this.name, this.version, {
registry: getNpmRegistryURL(),
token: this.#tokenStore?.get(getNpmRegistryURL())
});
if (!this.#hasSameSignatures(signatures, packumentVersionFromPublicRegistry.dist.signatures)) {
this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion"));
}
}
catch (err) {
const isScoped = Boolean(org);
if (isHTTPError(err) && err.statusCode === kNotFoundStatusCode && !isScoped) {
this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion_missing"));
}
}
}
catch {
// ignore
}
}
#hasSameSignatures(signatures, signaturesFromPublicRegistry) {
if (!signatures || !signaturesFromPublicRegistry) {
return false;
}
const sortedSignaturesFromPublic = signaturesFromPublicRegistry.sort((a, b) => a.keyid.localeCompare(b.keyid));
const sortedSignaturesFromPrivate = signatures.sort((a, b) => a.keyid.localeCompare(b.keyid));
return sortedSignaturesFromPrivate.length === signaturesFromPublicRegistry.length &&
sortedSignaturesFromPrivate?.every((signature, index) => signature.keyid === sortedSignaturesFromPublic[index].keyid
&& signature.sig === sortedSignaturesFromPublic[index].sig);
}
async enrichScopedDependencyConfusionWarnings(warnings, org) {
try {
await this.#npmApiClient.org(this.name);
}
catch (err) {
if (isHTTPError(err) && err.statusCode === kNotFoundStatusCode) {
await this.#addDependencyConfusionWarning(warnings, await i18n.getToken("scanner.dependency_confusion_missing_org", org));
}
}
}
async #addDependencyConfusionWarning(warnings, message) {
warnings.push({
type: "dependency-confusion",
message,
metadata: {
name: this.name
}
});
}
}
//# sourceMappingURL=NpmRegistryProvider.js.map