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
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ String inputs are bounded before query construction:
| `versionStart`, `versionEnd` | 256 characters each |
| CVSS metric fields | 256 characters, 32 slash-delimited tokens, and 32 characters per token |

Requests that exceed a limit return HTTP 400 before contacting OpenSearch. These bounds limit input size and query complexity without changing NVD-style CPE matching semantics.
Requests that exceed a limit return HTTP 400 before contacting OpenSearch. These bounds limit input size and query complexity without changing the documented CPE matching semantics.

Use `cveIds` to request up to 100 CVE records by comma-separated CVE ID.

Expand Down Expand Up @@ -150,13 +150,15 @@ curl -X POST "http://localhost:3000/search" \
-d '{"cpeName":"cpe:2.3:a:freetype:freetype:*:*:*:*:*:*:*:*"}'
```

The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product.
The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. In accordance with the CPE 2.3 Name Matching specification, CPE string literal comparisons are insensitive to lexical case, so an input component such as `macos` matches stored criteria containing `macOS`. Every concrete non-version component in the input, such as edition, language, target software, or target hardware, must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks. Optional input components set to `*` remain broad matches. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product.

The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. Numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This rule can produce different range behavior than a literal reading of source CVE Record applicability fields.

Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Criteria entry itself to be marked vulnerable. A vulnerable flag on an unrelated CPE entry does not satisfy the request. Omitting `isVulnerable`, or setting it to `false`, does not apply a vulnerability restriction.

### Virtual Match String Search

Use `virtualMatchString` when a user needs NVD-style CPE Match Criteria compatibility matching instead of concrete asset matching. Omitted trailing components and `*` components act as broad matches, matching NVD's broad CPE Match String behavior.
Use `virtualMatchString` when a user needs CPE Match Criteria compatibility matching instead of concrete asset matching. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches.

Partial product example:

Expand Down Expand Up @@ -186,7 +188,11 @@ curl -X POST "http://localhost:3000/search" \

Version ranges use overlap semantics. A stored wildcard-version CPE Match Criteria entry such as `cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*` with `versionEndExcluding=6.9` matches requested ranges that overlap versions before `6.9`, and does not match a requested range that starts at `6.9`.

Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. That would intentionally diverge from NVD behavior and should be handled as a policy decision before implementation.
CPE version comparisons used by both `cpeName` applicability and `virtualMatchString` version ranges use Maven-style qualifier precedence: `alpha`/`a` < `beta`/`b` < `milestone`/`m` < `rc`/`cr` < `snapshot` < an unqualified release/`ga`/`final`/`release` < `sp` < unknown qualifiers. Unknown qualifiers, including `hf`, sort after known qualifiers in case-insensitive lexical order. This means a range beginning at `1.0.0-rc1` includes `1.0.0`, while a range ending before `1.0.0` can include prerelease versions.

Numeric segments used in version range comparisons ignore leading zeros. A stored exclusive upper bound of `10.05.1` therefore includes `10.5.0`, and CVE-2026-33280's exclusive `6.02` upper bound includes `6.1` but excludes `6.2`. This normalization applies only to range comparison. Exact CPE Match Criteria preserve leading zeros, so the CVE-2025-9392 versions `1.0.04.001` and `1.0.4.1` are not equivalent. This comparison rule is not a universal firmware-versioning rule and can differ from a vendor's actual release sequence.

Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. Narrowing the accepted searches should be handled as a policy decision before implementation.

`POST /webSearch` is deprecated in favor of `POST /search`. It remains enabled by default during consumer migration and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch.

Expand Down
10 changes: 5 additions & 5 deletions api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@
"type": "string",
"maxLength": 2048,
"example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*",
"description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches."
"description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches."
},
"isVulnerable": {
"type": "boolean",
Expand Down Expand Up @@ -469,7 +469,7 @@
"type": "string",
"maxLength": 256,
"example": "2.2",
"description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component."
"description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component."
},
"versionStartType": {
"type": "string",
Expand All @@ -483,7 +483,7 @@
"type": "string",
"maxLength": 256,
"example": "2.6",
"description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component."
"description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component."
},
"versionEndType": {
"type": "string",
Expand All @@ -497,10 +497,10 @@
"type": "string",
"maxLength": 2048,
"example": "cpe:2.3:o:microsoft:windows_10_1809",
"description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component."
"description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. CPE string literal comparisons are insensitive to lexical case. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component."
}
}
}
}
}
}
}
18 changes: 18 additions & 0 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,29 @@ describe('OpenAPI contract', () => {
expect(schema.properties.noRejected.description).toContain('When true');
expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry');
expect(schema.properties.isVulnerable.description).toContain('False or omitted');
expect(schema.properties.cpeName.description).toContain('Maven-style ordering');
expect(schema.properties.cpeName.description).toContain(
'Every concrete non-version component must be compatible with the same CPE Match Criteria entry'
);
expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches');
expect(schema.properties.cpeName.description).toContain('insensitive to lexical case');
expect(schema.properties.cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros');
expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES);
expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES);
expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT);
expect(schema.properties.cveId).toBeUndefined();
expect(schema.properties.versionStart.description).toContain('versionStartType');
expect(schema.properties.versionStart.description).toContain('less than versionEnd');
expect(schema.properties.versionStart.description).toContain('ranges ignore leading zeros');
expect(schema.properties.versionStart.description).toContain('Maven-style ordering');
expect(schema.properties.versionEnd.description).toContain('versionEndType');
expect(schema.properties.versionEnd.description).toContain('exact CPE Match Criteria preserve leading zeros');
expect(schema.properties.versionEnd.description).toContain(
'prerelease qualifiers before an unqualified release'
);
expect(schema.properties.virtualMatchString.description).toContain('CPE Match Criteria');
expect(schema.properties.virtualMatchString.description).toContain('compatible');
expect(schema.properties.virtualMatchString.description).toContain('insensitive to lexical case');
expect(schema.properties.virtualMatchString.description).toContain('without a version component');
expect(schema.properties.virtualMatchString.example).toBe('cpe:2.3:o:microsoft:windows_10_1809');
expect(Object.keys(mediaType.examples)).toStrictEqual([
Expand Down Expand Up @@ -115,6 +129,10 @@ describe('OpenAPI contract', () => {
expect(cpeName.description).toContain('CPE Match Criteria');
expect(cpeName.description).toContain('part, vendor, product, and version');
expect(cpeName.description).toContain('version range fields');
expect(cpeName.description).toContain('insensitive to lexical case');
expect(cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros');
expect(cpeName.description).toContain('logical value NA');
expect(cpeName.description).toContain('numeric range boundaries');
expect(cpeName.description).toContain('HTTP 404');
expect(cpeName.description).toContain('virtualMatchString');
expect(cpeName.example).toBe('cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*');
Expand Down
2 changes: 1 addition & 1 deletion controllers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function validateParams(req, res, next) {
return res.status(400).json(error.missingCpeNameError());
}

//Ensures cpeName follows NVD API requirements for required CPE components
//Ensures cpeName contains the required concrete CPE components
if (params?.cpeName && !cpe.isValidCpeName(params.cpeName)) {
return res.status(404).json(error.invalidCpeNameError());
}
Expand Down
29 changes: 28 additions & 1 deletion controllers/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('middleware validation function', () => {
);
});

test('ValidateParams calls next() for broad NVD-style virtualMatchString values', () => {
test('ValidateParams accepts broad virtualMatchString values', () => {
validate({
virtualMatchString: 'cpe:2.3:o:*:windows_10_1809'
});
Expand Down Expand Up @@ -226,6 +226,33 @@ describe('middleware validation function', () => {
expectValidationSuccess();
});

test('ValidateParams accepts a prerelease-to-release version range', () => {
validate({
virtualMatchString: 'cpe:2.3:a:vendor:product',
versionStart: '1.0.0-rc1',
versionStartType: 'including',
versionEnd: '1.0.0',
versionEndType: 'including'
});

expectValidationSuccess();
});

test('ValidateParams rejects a release-to-prerelease version range', () => {
validate({
virtualMatchString: 'cpe:2.3:a:vendor:product',
versionStart: '1.0.0',
versionStartType: 'including',
versionEnd: '1.0.0-rc1',
versionEndType: 'including'
});

expectValidationError(
'VERSION_RANGE_ORDER',
'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.'
);
});

test('ValidateParams returns an error when version range params are used without virtualMatchString', () => {
validate({
versionStart: '2.2',
Expand Down
Loading
Loading