Skip to content

Commit bff4869

Browse files
committed
Added malware advisory cutoff date, to ignore old advisories
1 parent 0b4d3c4 commit bff4869

File tree

3 files changed

+54
-6
lines changed

3 files changed

+54
-6
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ npm run start -- --sync-sboms --enterprise ent --base-url https://github.interna
6767
| `--sync-malware` | Fetch & cache malware advisories (MALWARE classification). Requires a GitHub token |
6868
| `--match-malware` | Match current SBOM set against cached advisories |
6969
| `--malware-cache <dir>` | Advisory cache directory (required with malware operations) |
70+
| `--malware-cutoff <ISO-date>` | Ignore advisories whose publishedAt AND updatedAt are both before this date/time (e.g. `2025-09-29` or full timestamp) |
7071
| `--sarif-dir <dir>` | Write SARIF 2.1.0 files per repository (with malware matches) |
7172
| `--upload-sarif` | Upload generated SARIF to Code Scanning (requires --match-malware & --sarif-dir and a GitHub token) |
7273
| `--concurrency <n>` | Parallel SBOM fetches (default 5) |
@@ -144,6 +145,27 @@ npm run start -- --sbom-cache sboms --malware-cache malware-cache --match-malwar
144145

145146
If you also perform a search in the same invocation (add `--purl` or `--purl-file`), the JSON file will contain both `malwareMatches` and `search` top-level keys.
146147

148+
#### Advisory Date Cutoff
149+
150+
Use `--malware-cutoff` to exclude older advisories from matching. An advisory will be skipped if **both** its `publishedAt` and `updatedAt` timestamps are strictly earlier than the cutoff.
151+
152+
Accepted formats:
153+
154+
- Plain date: `YYYY-MM-DD` (interpreted as `YYYY-MM-DDT00:00:00.000Z`)
155+
- Full ISO timestamp: e.g. `2025-09-29T15:30:00Z`
156+
157+
Examples:
158+
159+
```bash
160+
# Ignore advisories published & last updated entirely before Sept 29 2025
161+
npm run start -- --sbom-cache sboms --malware-cache malware-cache --match-malware --malware-cutoff 2025-09-29
162+
163+
# Using a precise timestamp (keep advisories updated later that day UTC)
164+
npm run start -- --sbom-cache sboms --malware-cache malware-cache --match-malware --malware-cutoff 2025-09-29T12:00:00Z
165+
```
166+
167+
Rationale: This lets you focus on newly introduced / recently changed malware advisories (e.g., during incremental monitoring) without re-reporting older historical matches. Advisories updated after the cutoff remain eligible even if originally published earlier.
168+
147169
### Progress bar & log noise suppression
148170

149171
When collecting a large number of SBOMs you can enable a lightweight progress bar:

src/cli.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ async function main() {
3131
.option("match-malware", { type: "boolean", default: false, describe: "After sync/load, match SBOM packages against malware advisories" })
3232
.option("sarif-dir", { type: "string", describe: "Directory to write SARIF 2.1.0 files (one per repository) when --match-malware is used" })
3333
.option("upload-sarif", { type: "boolean", default: false, describe: "Upload generated SARIF (per-repo) to the Code Scanning API (requires --match-malware)" })
34+
.option("malware-cutoff", { type: "string", describe: "Ignore advisories whose publishedAt and updatedAt are both before this ISO date (e.g. 2025-09-29)" })
3435
.option("purl-file", { type: "string", describe: "Path to file with PURL queries (one per line; supports version ranges & wildcards; # or // for comments)" })
3536
.option("json", { type: "boolean", describe: "Emit search results as JSON to stdout (suppresses human output unless --cli also provided)" })
3637
.option("cli", { type: "boolean", describe: "When used with --json, also emit human-readable CLI output" })
@@ -45,7 +46,7 @@ async function main() {
4546
if (!args.sbomCache) throw new Error("Offline mode requires --sbom-cache (omit --sync-sboms)");
4647
}
4748
// If --cli is specified in combination with JSON or CSV, require an output file to avoid mixed stdout streams.
48-
if (args.cli && !args.outputFile && ( args.json || args.csv ) ) {
49+
if (args.cli && !args.outputFile && (args.json || args.csv)) {
4950
throw new Error("--cli with --json or --csv requires --output-file to avoid interleaving JSON and human output on stdout.");
5051
}
5152
// check that --malware-cache is provided
@@ -118,7 +119,7 @@ async function main() {
118119
let malwareMatches: import("./malwareMatcher.js").MalwareMatch[] | undefined;
119120
if (argv["match-malware"]) {
120121
const { matchMalware, buildSarifPerRepo, writeSarifFiles, uploadSarifPerRepo } = await import("./malwareMatcher.js");
121-
malwareMatches = matchMalware(mas.getAdvisories(), sboms);
122+
malwareMatches = matchMalware(mas.getAdvisories(), sboms, { advisoryDateCutoff: argv["malware-cutoff"] as string | undefined });
122123
if (!quiet) console.log(chalk.magenta(`Malware matches found: ${malwareMatches?.length ?? 0}`));
123124
if (malwareMatches) {
124125
if (!quiet) {
@@ -220,7 +221,7 @@ async function main() {
220221
if (argv.csv) {
221222
const fs = await import("fs");
222223
// Collect search data if searches were run; reconstruct from collector if we have combinedPurls
223-
let searchRows: Array<{ repo: string; purl: string; reason: string }> = [];
224+
const searchRows: Array<{ repo: string; purl: string; reason: string }> = [];
224225
if (combinedPurls.length) {
225226
const map = collector.searchByPurlsWithReasons(combinedPurls);
226227
for (const [repo, entries] of map.entries()) {
@@ -236,7 +237,7 @@ async function main() {
236237
}
237238
}
238239
// CSV columns: type,repo,purl,reason_or_advisory,range,updatedAt
239-
const header = ["type","repo","purl","reason_or_advisory","range","updatedAt"];
240+
const header = ["type", "repo", "purl", "reason_or_advisory", "range", "updatedAt"];
240241
const sanitize = (val: unknown): string => {
241242
if (val === null || val === undefined) return "";
242243
let s = String(val);

src/malwareMatcher.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,40 @@ function versionInRange(ecosystem: string, version: string | null, range: string
7171
return false;
7272
}
7373

74-
export function matchMalware(advisories: MalwareAdvisoryNode[], sboms: RepositorySbom[]): MalwareMatch[] {
74+
export interface MatchMalwareOptions {
75+
/** ISO date or datetime; advisories with both publishedAt and updatedAt before this are ignored */
76+
advisoryDateCutoff?: string;
77+
}
78+
79+
export function matchMalware(advisories: MalwareAdvisoryNode[], sboms: RepositorySbom[], opts?: MatchMalwareOptions): MalwareMatch[] {
7580
const matches: MalwareMatch[] = [];
7681

82+
// Parse cutoff (inclusive) – if only a date (YYYY-MM-DD) given, treat as 00:00:00Z that day
83+
let cutoffDate: Date | undefined;
84+
if (opts?.advisoryDateCutoff) {
85+
const raw = opts.advisoryDateCutoff.trim();
86+
let iso = raw;
87+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) iso = raw + 'T00:00:00.000Z';
88+
const parsed = Date.parse(iso);
89+
if (!isNaN(parsed)) {
90+
cutoffDate = new Date(parsed);
91+
} else {
92+
// eslint-disable-next-line no-console
93+
console.warn(`Ignoring invalid advisoryDateCutoff value: ${raw}`);
94+
}
95+
}
96+
7797
// Build advisory index keyed by ecosystem::name
7898
const index = new Map<string, MalwareAdvisoryNode[]>();
7999
for (const adv of advisories) {
80100
// Ignore advisories that have been withdrawn
81-
// Assumes MalwareAdvisoryNode has an optional withdrawnAt (string | null | undefined)
82101
if ((adv as unknown as { withdrawnAt?: string | null }).withdrawnAt) continue;
102+
// Ignore advisories older than cutoff (must be before cutoff in BOTH publishedAt & updatedAt to be excluded)
103+
if (cutoffDate) {
104+
const pub = new Date((adv as unknown as { publishedAt?: string }).publishedAt || 0);
105+
const upd = new Date((adv as unknown as { updatedAt?: string }).updatedAt || 0);
106+
if (pub < cutoffDate && upd < cutoffDate) continue;
107+
}
83108
for (const vuln of adv.vulnerabilities) {
84109
if (!vuln.name || !vuln.ecosystem) continue;
85110
const key = `${vuln.ecosystem}::${vuln.name}`.toLowerCase();

0 commit comments

Comments
 (0)