Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/sunny-knives-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@nodesecure/tree-walker": minor
"@nodesecure/scanner": minor
"@nodesecure/tarball": minor
"@nodesecure/rc": minor
---

feat(tarball): add warning when hostname resolve to a private ip
2 changes: 1 addition & 1 deletion workspaces/rc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"ajv": "6.12.6"
},
"dependencies": {
"@nodesecure/js-x-ray": "11.0.1",
"@nodesecure/js-x-ray": "11.2.0",
"@nodesecure/npm-types": "^1.2.0",
"@nodesecure/vulnera": "^2.0.1",
"@openally/config": "^1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion workspaces/scanner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"@nodesecure/contact": "^3.0.0",
"@nodesecure/flags": "^3.0.3",
"@nodesecure/i18n": "^4.0.2",
"@nodesecure/js-x-ray": "11.1.0",
"@nodesecure/js-x-ray": "11.2.0",
"@nodesecure/mama": "^2.1.0",
"@nodesecure/npm-registry-sdk": "^4.4.0",
"@nodesecure/npm-types": "^1.3.0",
Expand Down
3 changes: 2 additions & 1 deletion workspaces/tarball/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@
"dependencies": {
"@nodesecure/conformance": "^1.2.1",
"@nodesecure/fs-walk": "^2.0.0",
"@nodesecure/js-x-ray": "11.1.0",
"@nodesecure/js-x-ray": "11.2.0",
"@nodesecure/mama": "^2.1.0",
"@nodesecure/npm-types": "^1.2.0",
"@nodesecure/utils": "^2.3.0",
"ipaddr.js": "2.3.0",
"pacote": "^21.0.0"
},
"devDependencies": {
Expand Down
34 changes: 34 additions & 0 deletions workspaces/tarball/src/class/DnsResolver.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Import Node.js Dependencies
import { lookup } from "node:dns/promises";
import { type LookupAddress } from "node:dns";

// Import Third-party Dependencies
import ipaddress from "ipaddr.js";

export interface Resolver {
isPrivateHost(hostname: string): Promise<boolean>;
}

export class DnsResolver implements Resolver {
async isPrivateHost(hostname: string) {
const ipAddressListDetails: LookupAddress[] = await lookup(hostname, { all: true });
const ipAddressList = ipAddressListDetails.map((ipAddressDetails) => ipAddressDetails.address);

return ipAddressList.some(this.#isPrivateIPAddress);
}

#isPrivateIPAddress(ipAddress: string): boolean {
let ip = ipaddress.parse(ipAddress);

if (ip instanceof ipaddress.IPv6 && ip.isIPv4MappedAddress()) {
ip = ip.toIPv4Address();
}

const range = ip.range();
if (range !== "unicast") {
return true;
}

return false;
}
}
47 changes: 45 additions & 2 deletions workspaces/tarball/src/class/NpmTarball.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from "@nodesecure/mama";
import {
AstAnalyser,
CollectableSet,
warnings,
type AstAnalyserOptions
} from "@nodesecure/js-x-ray";

Expand All @@ -21,13 +23,18 @@ import {
getTarballComposition,
type TarballComposition
} from "../utils/index.ts";
import { type Resolver, DnsResolver } from "./DnsResolver.class.ts";

export interface ScannedFilesResult {
composition: TarballComposition;
conformance: conformance.SpdxExtractedResult;
code: SourceCodeReport;
}

export type NpmTarballOptions = {
resolver?: Resolver;
};

export class NpmTarball {
static JS_EXTENSIONS = new Set([
".js", ".mjs", ".cjs",
Expand All @@ -36,15 +43,18 @@ export class NpmTarball {
]);

manifest: LocatedManifestManager;
#resolver: Resolver;

constructor(
mama: ManifestManager
mama: ManifestManager,
options: NpmTarballOptions = {}
) {
if (!ManifestManager.isLocated(mama)) {
throw new Error("ManifestManager must have a location");
}

this.manifest = mama;
this.#resolver = options?.resolver ?? new DnsResolver();
}

async scanFiles(
Expand All @@ -64,14 +74,38 @@ export class NpmTarball {
code = new SourceCodeReport();
}
else {
const astAnalyser = new AstAnalyser(astAnalyserOptions);
const options = this.#optionsWithHostnameSet(astAnalyserOptions ?? {});

const hostNameSet = options?.collectables?.find((collectable) => collectable.type === "hostname")!;

const astAnalyser = new AstAnalyser(options);

code = await new SourceCodeScanner(this.manifest, { astAnalyser }).iterate({
manifest: [...this.manifest.getEntryFiles()]
.flatMap(filterJavaScriptFiles()),
javascript: composition.files
.flatMap(filterJavaScriptFiles())
});

const operationQueue =
Array.from(hostNameSet)
.map(({ value, locations }) => this.#resolver.isPrivateHost(value)
.then((isPrivate) => {
if (isPrivate) {
locations.forEach(({ file, location }) => {
code.warnings.push({
kind: "shady-link",
...warnings["shady-link"],
file: file ?? undefined,
location,
value,
source: "Scanner"
});
});
}
})
);
await Promise.allSettled(operationQueue);
}

return {
Expand All @@ -80,6 +114,15 @@ export class NpmTarball {
code
};
}

#optionsWithHostnameSet(options: AstAnalyserOptions): AstAnalyserOptions {
const hasHostnameSet = options?.collectables?.some((collectable) => collectable.type === "hostname");
if (hasHostnameSet) {
return options;
}

return { ...options, collectables: [...options.collectables ?? [], new CollectableSet("hostname")] };
}
}

function filterJavaScriptFiles() {
Expand Down
141 changes: 141 additions & 0 deletions workspaces/tarball/test/NpmTarball.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Import Node.js Dependencies
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, test } from "node:test";
import assert from "node:assert";

// Import Third-party Dependencies
import { CollectableSet, warnings, type Warning } from "@nodesecure/js-x-ray";
import { ManifestManager } from "@nodesecure/mama";

type SourceArrayLocation = [[number, number], [number, number]];

// Import Internal Dependencies
import { NpmTarball } from "../src/class/NpmTarball.class.ts";

// CONSTANTS
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const kFixturePath = path.join(__dirname, "fixtures", "npmTarball");
const kShadyLinkPath = path.join(kFixturePath, "shady-link");

describe("NpmTarball", () => {
test("it should have a shady-link warning when a hostname resolve a private ip address with collectables", async() => {
const mama = await ManifestManager.fromPackageJSON(path.join(kFixturePath, "shady-link", "package.json"));
const npmTarball = new NpmTarball(mama);
const hostnameSet = new CollectableSet("hostname");

const result = await npmTarball.scanFiles({
collectables: [hostnameSet]
});

assert.deepEqual(
result.code.warnings.sort(compareWarning),
[{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-1`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[3, 19], [3, 51]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-2`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
file: `${kShadyLinkPath}/private-ip-2`,
value: "192-168-1-250.sslip.io"
}].sort(compareWarning)
);
});

test("it should have a shady-link warning when a hostname resolve a private ip address without options", async() => {
const mama = await ManifestManager.fromPackageJSON(path.join(kFixturePath, "shady-link", "package.json"));
const npmTarball = new NpmTarball(mama);

const result = await npmTarball.scanFiles();

assert.deepEqual(
result.code.warnings.sort(compareWarning),
[{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-1`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[3, 19], [3, 51]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-2`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
file: `${kShadyLinkPath}/private-ip-2`,
value: "192-168-1-250.sslip.io"
}].sort(compareWarning)
);
});

test("it should have a shady-link warning when a hostname resolve a private ip address without hostname set", async() => {
const mama = await ManifestManager.fromPackageJSON(path.join(kFixturePath, "shady-link", "package.json"));
const npmTarball = new NpmTarball(mama);

const result = await npmTarball.scanFiles({
collectables: [new CollectableSet("url"), new CollectableSet("ip")]
});

assert.deepEqual(
result.code.warnings.sort(compareWarning),
[{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-1`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[3, 19], [3, 51]]] as SourceArrayLocation[],
source: "Scanner",
value: "10.0.0.1.sslip.io",
file: `${kShadyLinkPath}/private-ip-2`
},
{
...warnings["shady-link"],
kind: "shady-link",
location: [[[1, 18], [1, 50]]] as SourceArrayLocation[],
source: "Scanner",
file: `${kShadyLinkPath}/private-ip-2`,
value: "192-168-1-250.sslip.io"
}].sort(compareWarning)
);
});
});

function compareWarning(a: Warning, b: Warning): number {
const fileComparison = a.file?.localeCompare(b.file ?? "");

if (fileComparison) {
return fileComparison;
}

return (a.value ?? "")?.localeCompare(b.value ?? "");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const invalidIp = "https://example.invalid";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": "0.1.0",
"name": "shady-link",
"type": "module",
"license": "MIT"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const privateIp = "https://10.0.0.1.sslip.io/path";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const privateIp = "https://192-168-1-250.sslip.io";

const privateIp2 = "https://10.0.0.1.sslip.io/path";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const publicIp = "https://example.com";
2 changes: 1 addition & 1 deletion workspaces/tree-walker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"homepage": "https://github.com/NodeSecure/tree/master/workspaces/tree-walker#readme",
"dependencies": {
"@nodesecure/js-x-ray": "11.0.1",
"@nodesecure/js-x-ray": "11.2.0",
"@nodesecure/npm-registry-sdk": "^4.0.0",
"@nodesecure/npm-types": "^1.1.0",
"@npmcli/arborist": "9.1.9",
Expand Down