8000 Migrate parser-sdk to typescript by J12934 · Pull Request #3254 · secureCodeBox/secureCodeBox · GitHub
[go: up one dir, main page]

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: 4 additions & 4 deletions .templates/new-scanner/parser/parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ test("should properly parse new-scanner json file", async () => {
const fileContent = JSON.parse(
await readFile(import.meta.dirname + "/__testFiles__/example.com.json", {
encoding: "utf8",
})
}),
);
const findings = await parse(fileContent);
// validate findings
await expect(validateParser(findings)).resolves.toBeUndefined();
expect(validateParser(findings)).toBeUndefined();
expect(findings).toMatchInlineSnapshot();
});

test("should properly parse empty json file", async () => {
const fileContent = JSON.parse(
await readFile(import.meta.dirname + "/__testFiles__/empty.json", {
encoding: "utf8",
})
}),
);
const findings = await parse(fileContent);
// validate findings
await expect(validateParser(findings)).resolves.toBeUndefined();
expect(validateParser(findings)).toBeUndefined();
expect(findings).toMatchInlineSnapshot();
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ COPY --from=build --chown=root:root --chmod=755 /home/app/node_modules/ ./node_m
COPY --chown=root:root --chmod=755 ./parser.js ./parser.js
```

If your parser does not require any external dependencies, A multi-stage build is not needed.
If your parser does not require any external dependencies, A multi-stage build is not needed.
Instead, a simpler Dockerfile can be used.

```dockerfile
Expand Down Expand Up @@ -94,7 +94,7 @@ Please provide some tests for your parser in the `parser.test.js` file. To make
import { validateParser } from "@securecodebox/parser-sdk-nodejs/parser-utils";

const findings = await parse(fileContent);
await expect(validateParser(findings)).resolves.toBeUndefined();
expect(validateParser(findings)).toBeUndefined();
```

If you need additional files for your test please save these in the `__testFiles__` directory. Please take a look at [Integration Tests | secureCodeBox](/docs/contributing/integrating-a-scanner/integration-tests) for more information.
Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/contributing/test-concept/scanner-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ We employ two types of tests: Unit tests for the parser and integration-tests. B

### Unit Tests for Parser

Each scanner has a parser and each parser has a unit test file. The unit test file is named parser.test.js. This file contains different test scenarios. In each test, the results from parser.js and the folder `_snapshots_` are compared. If they are the same, the unit test is successful.
Each scanner has a parser and each parser has a unit test file. The unit test file is named parser.test.js. This file contains different test scenarios. In each test, the results from parser.js and the folder `_snapshots_` are compared. If they are the same, the unit test is successful.
A unit test can look like this:

```js
Expand All @@ -24,7 +24,7 @@ test("parser parses large json result without vulnerable extensions successfully
}
);
const findings = await parse(JSON.parse(fileContent));
await expect(validateParser(findings)).resolves.toBeUndefined();
expect(validateParser(findings)).toBeUndefined();
expect(findings).toMatchSnapshot();
});

Expand Down
3 changes: 1 addition & 2 deletions parser-sdk/nodejs/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ FROM oven/bun:1.2 AS build
WORKDIR /home/app/
COPY package.json package-lock.json ./
RUN bun install --ignore-scripts
COPY *.js ./
COPY *.ts findings-schema.json ./
RUN bun run build

FROM node:22-alpine
ARG NODE_ENV
RUN addgroup --system --gid 1001 app && adduser app --system --uid 1001 --ingroup app
WORKDIR /home/app/parser-wrapper/
COPY --chown=root:root --chmod=755 ./package.json ./package-lock.json ./
COPY --chown=root:root --chmod=755 ./findings-schema.json ./findings-schema.json
COPY --from=build --chown=root:root --chmod=755 /home/app/build/ ./
USER 1001
ENV NODE_ENV=${NODE_ENV:-production}
Expand Down
32 changes: 17 additions & 15 deletions parser-sdk/nodejs/package-lock.json

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

4 changes: 3 additions & 1 deletion parser-sdk/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
"ajv-formats": "^3.0.1",
"jsonpointer": "^5.0.1"
},
"devDependencies": {}
"devDependencies": {
"@types/node": "^22.18.0"
}
}
81 changes: 0 additions & 81 deletions parser-sdk/nodejs/parser-utils.js

This file was deleted.

136 changes: 136 additions & 0 deletions parser-sdk/nodejs/parser-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

import { randomUUID } from "node:crypto";

import addFormats from "ajv-formats";
import { get } from "jsonpointer";
import Ajv, { type ErrorObject } from "ajv-draft-04";
import findingsSchema from "./findings-schema.json" with { type: "json" };

const ajv = new Ajv();
addFormats(ajv);

export type Severity = "INFORMATIONAL" | "LOW" | "MEDIUM" | "HIGH";

export interface Reference {
type: string;
value: string;
}

export interface ScanSummary {
created_at: string; // ISO8601 date-time
name: string;
namespace: string;
scan_type: string;
}

// parsers do not need to set all fields as fields like the ID are set by the parser-sdk
export interface FindingFromParser {
identified_at?: string | null; // ISO8601 date-time
name: string;
description?: string | null;
category: string;
severity: Severity;
mitigation?: string | null;
references?: Reference[] | null;
attributes?: Record<string, unknown>;
location?: string | null;
}

export interface FindingWithIdsAndDates extends FindingFromParser {
id: string; // UUID v4
parsed_at: string; // ISO8601 date-time
}

export interface Finding extends FindingWithIdsAndDates {
scan: ScanSummary;
}

export interface Scan {
metadata: {
name: string;
namespace: string;
creationTimestamp: string;
};
spec: {
scanType: string;
};
status: {
rawResultType: string;
};
}

export function validate(findings: unknown): asserts findings is Finding[] {
const validator = ajv.compile(findingsSchema);
const valid = validator(findings);
if (!valid && validator.errors) {
const errorMessage = generateErrorMessage(validator.errors, findings);
throw new Error(errorMessage);
} else if (!valid) {
throw new Error("Validation of findings failed for unknown reasons.");
}
}

export function addScanMetadata(
findings: FindingWithIdsAndDates[],
scan: Scan,
): Finding[] {
const scanMetadata = {
created_at: scan.metadata.creationTimestamp,
name: scan.metadata.name,
namespace: scan.metadata.namespace,
scan_type: scan.spec.scanType,
};

return findings.map((finding) => ({
...finding,
scan: scanMetadata,
}));
}

export function addIdsAndDates(
findings: FindingFromParser[],
): FindingWithIdsAndDates[] {
return findings.map((finding) => {
return {
...finding,
id: randomUUID(),
parsed_at: new Date().toISOString(),
};
});
}

// used for tests to validate if the parser sets all required fields correctly. Adds sample IDs and Dates to the findings which would normally be set by the parser-sdk.
export function validateParser(findings: FindingFromParser[]) {
const sampleScan: Scan = {
metadata: {
creationTimestamp: new Date().toISOString(),
name: "sample-scan-name",
namespace: "sample-namespace",
},
spec: {
scanType: "sample-scan-type",
},
status: {
rawResultType: "example-results",
},
};
// add sample IDs and Dates only if the findings Array is not empty
const extendedData = addScanMetadata(addIdsAndDates(findings), sampleScan);
return validate(extendedData);
}

function generateErrorMessage(errors: ErrorObject[], findings: Finding[]) {
return JSON.stringify(
errors.map((error) => {
return {
...error,
invalidValue: get(findings, error.instancePath),
};
}),
null,
2,
);
}
Loading
Loading
0