Skip to content

Commit

Permalink
fix: use object-hash & expose utils from mama for Scanner (#263)
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Jul 8, 2024
1 parent f626feb commit 7fc00f6
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 191 deletions.
1 change: 1 addition & 0 deletions package-lock.json

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

154 changes: 154 additions & 0 deletions workspaces/mama/src/ManifestManager.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Import Node.js Dependencies
import fs from "node:fs/promises";
import path from "node:path";

// Import Third-party Dependencies
import { parseAuthor } from "@nodesecure/utils";
import type {
PackumentVersion, PackageJSON, WorkspacesPackageJSON, Contact
} from "@nodesecure/npm-types";

// Import Internal Dependencies
import { packageJSONIntegrityHash } from "./utils/index.js";

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }

export type NonOptionalPackageJSONProperties =
"dependencies" |
"devDependencies" |
"scripts" |
"gypfile";

// CONSTANTS
const kNativeNpmPackages = new Set([
"node-gyp",
"node-pre-gyp",
"node-gyp-build",
"node-addon-api"
]);

/**
* @see https://www.nerdycode.com/prevent-npm-executing-scripts-security/
*/
export const kUnsafeNPMScripts = new Set([
"install",
"preinstall",
"postinstall",
"preuninstall",
"postuninstall"
]);

export type ManifestManagerDefaultProperties = Required<
Pick<PackumentVersion, NonOptionalPackageJSONProperties>
>;

export type ManifestManagerDocument =
PackageJSON |
WorkspacesPackageJSON |
PackumentVersion;

export class ManifestManager<
MetadataDef extends Record<string, any> = Record<string, any>
> {
static Default: Readonly<ManifestManagerDefaultProperties> = Object.freeze({
dependencies: {},
devDependencies: {},
scripts: {},
gypfile: false
});

public metadata: MetadataDef = Object.create(null);
public document: WithRequired<
ManifestManagerDocument,
NonOptionalPackageJSONProperties
>;

public flags = Object.seal({
hasUnsafeScripts: false,
isNative: false
});

constructor(
document: ManifestManagerDocument
) {
this.document = Object.assign(
{ ...ManifestManager.Default },
structuredClone(document)
);

this.flags.isNative = [
...this.dependencies,
...this.devDependencies
].some((pkg) => kNativeNpmPackages.has(pkg)) || this.document.gypfile;
this.flags.hasUnsafeScripts = Object
.keys(this.document.scripts)
.some((script) => kUnsafeNPMScripts.has(script.toLowerCase()));
}

get nodejsImports() {
return this.document.imports ?? {};
}

get dependencies() {
return Object.keys(this.document.dependencies);
}

get devDependencies() {
return Object.keys(this.document.devDependencies);
}

get spec(): `${string}@${string}` {
const hasBothProperties = ["name", "version"]
.every((key) => key in this.document);
if (this.isWorkspace && !hasBothProperties) {
throw new Error("spec is not available for the given workspace");
}

return `${this.document.name}@${this.document.version}`;
}

get author(): Contact | null {
return parseAuthor(this.document.author);
}

get isWorkspace(): boolean {
return "workspaces" in this.document;
}

get integrity(): string {
if (this.isWorkspace) {
throw new Error("integrity is not available for workspaces");
}

return packageJSONIntegrityHash(this.document);
}

static async fromPackageJSON(
location: string
): Promise<ManifestManager> {
if (typeof location !== "string") {
throw new TypeError("location must be a string primitive");
}

const packageLocation = location.endsWith("package.json") ?
location :
path.join(location, "package.json");
const packageStr = await fs.readFile(packageLocation, "utf-8");

try {
const packageJSON = JSON.parse(
packageStr
) as PackageJSON | WorkspacesPackageJSON;

return new ManifestManager(
packageJSON
);
}
catch (cause) {
throw new Error(
`Failed to parse package.json located at: ${packageLocation}`,
{ cause }
);
}
}
}
162 changes: 4 additions & 158 deletions workspaces/mama/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,158 +1,4 @@
// Import Node.js Dependencies
import fs from "node:fs/promises";
import path from "node:path";

// Import Third-party Dependencies
import hash from "object-hash";
import { parseAuthor } from "@nodesecure/utils";
import type {
PackumentVersion, PackageJSON, WorkspacesPackageJSON, Contact
} from "@nodesecure/npm-types";

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }

export type NonOptionalPackageJSONProperties =
"dependencies" |
"devDependencies" |
"scripts" |
"gypfile";

// CONSTANTS
const kNativeNpmPackages = new Set([
"node-gyp",
"node-pre-gyp",
"node-gyp-build",
"node-addon-api"
]);

/**
* @see https://www.nerdycode.com/prevent-npm-executing-scripts-security/
*/
export const kUnsafeNPMScripts = new Set([
"install",
"preinstall",
"postinstall",
"preuninstall",
"postuninstall"
]);

export type ManifestManagerDefaultProperties = Required<
Pick<PackumentVersion, NonOptionalPackageJSONProperties>
>;

export type ManifestManagerDocument =
PackageJSON |
WorkspacesPackageJSON |
PackumentVersion;

export class ManifestManager<
MetadataDef extends Record<string, any> = Record<string, any>
> {
static Default: Readonly<ManifestManagerDefaultProperties> = Object.freeze({
dependencies: {},
devDependencies: {},
scripts: {},
gypfile: false
});

public metadata: MetadataDef = Object.create(null);
public document: WithRequired<
ManifestManagerDocument,
NonOptionalPackageJSONProperties
>;

public flags = Object.seal({
hasUnsafeScripts: false,
isNative: false
});

constructor(
document: ManifestManagerDocument
) {
this.document = Object.assign(
{ ...ManifestManager.Default },
structuredClone(document)
);

this.flags.isNative = [
...this.dependencies,
...this.devDependencies
].some((pkg) => kNativeNpmPackages.has(pkg)) || this.document.gypfile;
this.flags.hasUnsafeScripts = Object
.keys(this.document.scripts)
.some((script) => kUnsafeNPMScripts.has(script.toLowerCase()));
}

get nodejsImports() {
return this.document.imports ?? {};
}

get dependencies() {
return Object.keys(this.document.dependencies);
}

get devDependencies() {
return Object.keys(this.document.devDependencies);
}

get spec(): `${string}@${string}` {
const hasBothProperties = ["name", "version"]
.every((key) => key in this.document);
if (this.isWorkspace && !hasBothProperties) {
throw new Error("spec is not available for the given workspace");
}

return `${this.document.name}@${this.document.version}`;
}

get author(): Contact | null {
return parseAuthor(this.document.author);
}

get isWorkspace(): boolean {
return "workspaces" in this.document;
}

get integrity(): string {
if (this.isWorkspace) {
throw new Error("integrity is not available for workspaces");
}

return hash({
name: this.document.name,
version: this.document.version,
dependencies: this.document.dependencies,
license: this.document.license ?? "NONE",
scripts: this.document.scripts
});
}

static async fromPackageJSON(
location: string
): Promise<ManifestManager> {
if (typeof location !== "string") {
throw new TypeError("location must be a string primitive");
}

const packageLocation = location.endsWith("package.json") ?
location :
path.join(location, "package.json");
const packageStr = await fs.readFile(packageLocation, "utf-8");

try {
const packageJSON = JSON.parse(
packageStr
) as PackageJSON | WorkspacesPackageJSON;

return new ManifestManager(
packageJSON
);
}
catch (cause) {
throw new Error(
`Failed to parse package.json located at: ${packageLocation}`,
{ cause }
);
}
}
}
export * from "./ManifestManager.class.js";
export {
packageJSONIntegrityHash
} from "./utils/index.js";
1 change: 1 addition & 0 deletions workspaces/mama/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./integrity-hash.js";
37 changes: 37 additions & 0 deletions workspaces/mama/src/utils/integrity-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Import Third-party Dependencies
import hash from "object-hash";
import type {
PackumentVersion, PackageJSON, WorkspacesPackageJSON
} from "@nodesecure/npm-types";

export interface packageJSONIntegrityHashOptions {
/**
* Know whether the document comes from the NPM registry or a local tarball/project
*
* @default false
*/
isFromRemoteRegistry?: boolean;
}

export function packageJSONIntegrityHash(
document: PackumentVersion | PackageJSON | WorkspacesPackageJSON,
options: packageJSONIntegrityHashOptions = {}
) {
const { isFromRemoteRegistry = false } = options;
const { dependencies = {}, license = "NONE", scripts = {} } = document;

if (isFromRemoteRegistry) {
// See https://github.com/npm/cli/issues/5234
if ("install" in dependencies && dependencies.install === "node-gyp rebuild") {
delete dependencies.install;
}
}

return hash({
name: document.name,
version: document.version,
dependencies,
license,
scripts
});
}
Loading

0 comments on commit 7fc00f6

Please sign in to comment.