Skip to content

Commit ec4f55e

Browse files
authored
fix: enforce stricter number validation when using --nested-jars-depth flag (#736)
* fix: enforce stricter number validation when using --nested-jars-depth flag Replaced loose validation logic with a new isStrictNumber helper. This narrows the scope of valid inputs to strictly finite numbers, eliminating loose type coercion. Empty strings remain valid and resolve to the minimum nested-jars-depth of 1. This maintains parity with how other CLI flags handle default/empty states. BREAKING CHANGE: The application no longer accepts `true` as a valid numeric argument. * fix: ran npm to format linting * fix: added formatted changes * chore: updated snapshots * refactor: removed redundant call to Number.isNaN(num) * fix: resolve package-lock.json conflict * fix: handle empty/whitespace inputs for jar depth flags correctly - Added relevant test cases for empty strings, w/ and w/o spaces - Fixed asymmetric behavior between `shaded-jars-depth` and `nested-jars-depth` - Renamed `test/unit/options-utils.spec.ts` to match source filename * refactor: added helper function for resolving nested-jars params * fix: add check to prevent users from using both flags * chore: fixed linting issues * chore: switched .replace(...) for .trim()
1 parent 826944b commit ec4f55e

6 files changed

Lines changed: 369 additions & 14 deletions

File tree

lib/analyzer/static-analyzer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
getRpmSqliteDbFileContent,
5757
getRpmSqliteDbFileContentAction,
5858
} from "../inputs/rpm/static";
59+
import { resolveNestedJarsOption } from "../option-utils";
5960
import { isTrue } from "../option-utils";
6061
import { ImageType, ManifestFile, PluginOptions } from "../types";
6162
import {
@@ -318,8 +319,7 @@ export async function analyze(
318319
}
319320

320321
function getNestedJarsDesiredDepth(options: Partial<PluginOptions>) {
321-
const nestedJarsOption =
322-
options["nested-jars-depth"] || options["shaded-jars-depth"];
322+
const nestedJarsOption = resolveNestedJarsOption(options);
323323
let nestedJarsDepth = 1;
324324
const depthNumber = Number(nestedJarsOption);
325325
if (!isNaN(depthNumber) && depthNumber >= 0) {

lib/option-utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1-
export { isTrue, isNumber };
1+
import { PluginOptions } from "./types";
2+
export { isTrue, isNumber, isStrictNumber };
23

34
function isTrue(value?: boolean | string): boolean {
45
return String(value).toLowerCase() === "true";
56
}
67

8+
// This strictly follows the ECMAScript Language Specification: https://262.ecma-international.org/5.1/#sec-9.3
79
function isNumber(value?: boolean | string): boolean {
810
return !isNaN(Number(value));
911
}
12+
13+
// Must be a finite numeric value, excluding booleans, Infinity, and non-numeric strings
14+
function isStrictNumber(value?: boolean | string): boolean {
15+
if (typeof value === "boolean" || !value?.trim().length) {
16+
return false;
17+
}
18+
19+
const num = Number(value);
20+
return Number.isFinite(num);
21+
}
22+
23+
export function resolveNestedJarsOption(options?: Partial<PluginOptions>) {
24+
const safeOptions = options || {};
25+
26+
return [
27+
safeOptions["nested-jars-depth"],
28+
safeOptions["shaded-jars-depth"],
29+
].find(isDefined);
30+
}
31+
32+
export function isDefined(value?: string | boolean): boolean {
33+
return value !== "" && value != null;
34+
}

lib/scan.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import { ImageName } from "./extractor/image";
99
import { ExtractAction, ExtractionResult } from "./extractor/types";
1010
import { fullImageSavePath } from "./image-save-path";
1111
import { getArchivePath, getImageType } from "./image-type";
12-
import { isNumber, isTrue } from "./option-utils";
12+
import {
13+
isDefined,
14+
isStrictNumber,
15+
isTrue,
16+
resolveNestedJarsOption,
17+
} from "./option-utils";
1318
import * as staticModule from "./static";
1419
import { ImageType, PluginOptions, PluginResponse } from "./types";
1520
import { isValidDockerImageReference } from "./utils";
@@ -40,20 +45,24 @@ async function getAnalysisParameters(
4045
throw new Error("No image identifier or path provided");
4146
}
4247

43-
const nestedJarsDepth =
44-
options["nested-jars-depth"] || options["shaded-jars-depth"];
4548
if (
46-
(isTrue(nestedJarsDepth) || isNumber(nestedJarsDepth)) &&
47-
isTrue(options["exclude-app-vulns"])
49+
isDefined(options["shaded-jars-depth"]) &&
50+
isDefined(options["nested-jars-depth"])
4851
) {
52+
throw new Error(
53+
"Cannot use --shaded-jars-depth together with --nested-jars-depth, please use the latter",
54+
);
55+
}
56+
57+
const nestedJarsDepth = resolveNestedJarsOption(options);
58+
if (isStrictNumber(nestedJarsDepth) && isTrue(options["exclude-app-vulns"])) {
4959
throw new Error(
5060
"To use --nested-jars-depth, you must not use --exclude-app-vulns",
5161
);
5262
}
5363

5464
if (
55-
(!isNumber(nestedJarsDepth) &&
56-
!isTrue(nestedJarsDepth) &&
65+
(!isStrictNumber(nestedJarsDepth) &&
5766
typeof nestedJarsDepth !== "undefined") ||
5867
Number(nestedJarsDepth) < 0
5968
) {

test/lib/save/image.tar

-45.2 MB
Binary file not shown.

test/system/application-scans/java.spec.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,26 +96,25 @@ describe("jar binaries scanning", () => {
9696
imageNameAndTag = `docker-archive:${fixturePath}`;
9797
});
9898

99-
it("should return nested (second-level) jar in the result", async () => {
99+
it(`should unpack 1 level of jars if ${flagName} flag is missing`, async () => {
100100
// Act
101101
pluginResult = await scan({
102102
path: imageNameAndTag,
103103
"app-vulns": true,
104-
[flagName]: true,
105104
});
106105

107-
// Assert
108106
fingerprints =
109107
pluginResult.scanResults[1].facts[0].data.fingerprints;
110108

111109
expect(fingerprints).toContainEqual(nestedJar);
112110
});
113111

114-
it(`should unpack 1 level of jars if ${flagName} flag is missing`, async () => {
112+
it(`should unpack 1 level of jars if ${flagName} flag is ''`, async () => {
115113
// Act
116114
pluginResult = await scan({
117115
path: imageNameAndTag,
118116
"app-vulns": true,
117+
[flagName]: "",
119118
});
120119

121120
fingerprints =
@@ -146,6 +145,17 @@ describe("jar binaries scanning", () => {
146145
expect(fingerprints).not.toContainEqual(nestedJar);
147146
});
148147

148+
it(`should throw if ${flagName} flag is set to true`, async () => {
149+
// Act + Assert
150+
await expect(
151+
scan({
152+
path: imageNameAndTag,
153+
"app-vulns": true,
154+
[flagName]: true,
155+
}),
156+
).rejects.toThrow();
157+
});
158+
149159
it(`should throw if ${flagName} flag is set to -1`, async () => {
150160
// Act + Assert
151161
await expect(
@@ -157,6 +167,17 @@ describe("jar binaries scanning", () => {
157167
).rejects.toThrow();
158168
});
159169

170+
it(`should throw if ${flagName} flag is set to ' '`, async () => {
171+
// Act + Assert
172+
await expect(
173+
scan({
174+
path: imageNameAndTag,
175+
"app-vulns": true,
176+
[flagName]: " ",
177+
}),
178+
).rejects.toThrow();
179+
});
180+
160181
it("should throw error if exclude-app-vulns flag is true", async () => {
161182
// Act
162183
await expect(
@@ -467,5 +488,23 @@ describe("jar binaries scanning", () => {
467488
});
468489
},
469490
);
491+
describe("conflicting flags", () => {
492+
const fixturePath = getFixture(
493+
"docker-archives/docker-save/java-uberjar.tar",
494+
);
495+
const imageNameAndTag = `docker-archive:${fixturePath}`;
496+
497+
it(`should throw if both --shaded-jars-depth and --nested-jars-depth flags are set`, async () => {
498+
// Act + Assert
499+
await expect(
500+
scan({
501+
path: imageNameAndTag,
502+
"app-vulns": true,
503+
"shaded-jars-depth": "2",
504+
"nested-jars-depth": "4",
505+
}),
506+
).rejects.toThrow();
507+
});
508+
});
470509
});
471510
});

0 commit comments

Comments
 (0)