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
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ When filtering with `cpeName`, provide a CPE 2.3 name beginning with `cpe:2.3` a

### CPE Name Search

Use `cpeName` when a user has a concrete asset CPE Name and needs CVE records whose CPE Match Criteria apply to that asset version. The `part`, `vendor`, `product`, and `version` components must all be concrete values, not `*`.
Use `cpeName` when a user has a concrete asset CPE Name and needs CVE records whose CPE Match Criteria apply to that asset version. The `part`, `vendor`, `product`, and `version` components must all be concrete values. Unescaped `*` or `?` characters anywhere in those required components return HTTP 400 (`INVALID_CPE_NAME`), including versions such as `2.6.*`, `2.6*`, `2.*`, and `*.*`. Backslash-escaped literal characters remain allowed. Optional components can still be `*` or omitted.

Valid `cpeName` example:

Expand All @@ -174,7 +174,23 @@ curl -X POST "http://localhost:3000/search" \

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.
The requested version does not need to appear literally in a returned record when it falls within a stored range. The API evaluates those criteria; it does not verify that a software version actually exists. An open-ended range can therefore match a hypothetical version such as `1001.1`.

For a version interval, use `virtualMatchString` without a version component and supply explicit version boundaries. For example, to search for criteria overlapping Linux versions from `2.6` inclusive to `2.7` exclusive:

```json
{
"virtualMatchString": "cpe:2.3:o:linux:linux_kernel",
"versionStart": "2.6",
"versionStartType": "including",
"versionEnd": "2.7",
"versionEndType": "excluding"
}
```

An embedded wildcard such as `2.6.*` is not a supported `cpeName` version-family search. A literal version ending in punctuation, such as `2.6.`, is still accepted but is not a version-prefix search either.

The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies, not that the version is unknown or unrestricted. 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. The CPE specification defines `NA`; this treatment of numeric range boundaries is an API matching policy. This rule can produce different range behavior than a literal reading of source CVE Record applicability fields. Do not substitute `-` for `*` to search all versions; use `virtualMatchString` with an omitted version or a whole-component `*` instead.

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.

Expand Down
2 changes: 1 addition & 1 deletion api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,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 value must begin with cpe:2.3 and contain concrete part, vendor, product, and version components; part must be a, h, or o. Empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. 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 response includes a CPE_COMPONENTS_DEFAULTED warning when omitted trailing components are treated as wildcards. 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. 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. The value must begin with cpe:2.3 and contain concrete part, vendor, product, and version components; part must be a, h, or o. Unescaped * or ? characters anywhere in those required components, including versions such as 2.6.*, return HTTP 400. Backslash-escaped literal characters remain allowed. Empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. 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 response includes a CPE_COMPONENTS_DEFAULTED warning when omitted trailing components are treated as wildcards. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The requested version need not appear literally in a record when it falls within a range. The API does not verify that a software version actually exists; open-ended criteria can match hypothetical versions. 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, not an unknown or unrestricted version; numeric range boundaries on wildcard-version criteria do not exclude it under the API matching policy. Use virtualMatchString for partial CPE Match String searches or all versions of a product. For a version interval, omit the virtualMatchString version component and supply versionStart/versionStartType and/or versionEnd/versionEndType. A cpeName version ending in punctuation, such as 2.6., is not a version-prefix search."
},
"isVulnerable": {
"oneOf": [
Expand Down
5 changes: 5 additions & 0 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ describe('OpenAPI contract', () => {
expect(cpeName.description).toContain('logical value NA');
expect(cpeName.description).toContain('numeric range boundaries');
expect(cpeName.description).toContain('HTTP 400');
expect(cpeName.description).toContain('Unescaped * or ?');
expect(cpeName.description).toContain('Backslash-escaped literal characters remain allowed');
expect(cpeName.description).toContain('does not verify that a software version actually exists');
expect(cpeName.description).toContain('not an unknown or unrestricted version');
expect(cpeName.description).toContain('versionStart/versionStartType');
expect(cpeName.description).toContain('virtualMatchString');
expect(cpeName.example).toBe('cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*');
expect(openapi.paths['/search'].post.responses['404']).toBeUndefined();
Expand Down
82 changes: 44 additions & 38 deletions routes/cpeName.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,49 @@ describe('POST /search cpeName route behavior', () => {
jest.clearAllMocks();
});

test('rejects cpeName with wildcard version before the controller runs', async () => {
const cpeName = 'cpe:2.3:a:artifex:ghostscript:*:*:*:*:*:*:*:*';
const response = await request(app, {
method: 'POST',
path: '/search',
body: {
cpeName
}
});

expect(response.status).toBe(400);
expect(response.body).toStrictEqual({
error: 'INVALID_CPE_NAME',
message:
'The cpeName parameter must be a CPE 2.3 name with non-wildcard part, vendor, product, and version components.'
});
expect(controller.SEARCH).not.toHaveBeenCalled();
});
test.each(['*', '2.6.*', '2.6*', '2.*', '*.*', '0.*', '10.*', '100.*', '2.6.?'])(
'rejects cpeName version %s before the controller runs',
async (version) => {
const cpeName = `cpe:2.3:o:linux:linux_kernel:${version}:*:*:*:*:*:*:*`;
const response = await request(app, {
method: 'POST',
path: '/search',
body: {
cpeName
}
});

test('allows valid cpeName through to the controller', async () => {
const cpeName = 'cpe:2.3:a:artifex:ghostscript:10.05.1:*:*:*:*:*:*:*';
const response = await request(app, {
method: 'POST',
path: '/search',
body: {
cpeName
}
});

expect(response.status).toBe(200);
expect(controller.SEARCH).toHaveBeenCalledTimes(1);
expect(response.body).toStrictEqual({
route: 'search',
params: {
scope: 'FULL',
cpeName
}
});
});
expect(response.status).toBe(400);
expect(response.body).toStrictEqual({
error: 'INVALID_CPE_NAME',
message:
'The cpeName parameter must be a CPE 2.3 name with non-wildcard part, vendor, product, and version components.'
});
expect(controller.SEARCH).not.toHaveBeenCalled();
}
);

test.each(['10.05.1', '-', '2.6.', '1001.1', '5.1991234123412341234abcdefasdfasdfasdf', String.raw`2.6.\*`])(
'allows cpeName version %s through to the controller',
async (version) => {
const cpeName = `cpe:2.3:a:artifex:ghostscript:${version}:*:*:*:*:*:*:*`;
const response = await request(app, {
method: 'POST',
path: '/search',
body: {
cpeName
}
});

expect(response.status).toBe(200);
expect(controller.SEARCH).toHaveBeenCalledTimes(1);
expect(response.body).toStrictEqual({
route: 'search',
params: {
scope: 'FULL',
cpeName
}
});
}
);
});
2 changes: 1 addition & 1 deletion routes/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const searchRequestProperties = {
}
),
cpeName: searchRequestProperty(
'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The value must begin with cpe:2.3 and contain concrete part, vendor, product, and version components; part must be a, h, or o. Empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. 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 response includes a CPE_COMPONENTS_DEFAULTED warning when omitted trailing components are treated as wildcards. 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. Use virtualMatchString for partial CPE Match String searches.',
'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The value must begin with cpe:2.3 and contain concrete part, vendor, product, and version components; part must be a, h, or o. Unescaped * or ? characters anywhere in those required components, including versions such as 2.6.*, return HTTP 400. Backslash-escaped literal characters remain allowed. Empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. 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 response includes a CPE_COMPONENTS_DEFAULTED warning when omitted trailing components are treated as wildcards. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The requested version need not appear literally in a record when it falls within a range. The API does not verify that a software version actually exists; open-ended criteria can match hypothetical versions. 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, not an unknown or unrestricted version; numeric range boundaries on wildcard-version criteria do not exclude it under the API matching policy. Use virtualMatchString for partial CPE Match String searches or all versions of a product. For a version interval, omit the virtualMatchString version component and supply versionStart/versionStartType and/or versionEnd/versionEndType. A cpeName version ending in punctuation, such as 2.6., is not a version-prefix search.',
{
type: 'string',
maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName,
Expand Down
18 changes: 17 additions & 1 deletion utils/cpe.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ function hasUnescapedWhitespace(value) {
return false;
}

function hasUnescapedWildcard(value) {
let escaped = false;

for (const character of value) {
if (escaped) {
escaped = false;
} else if (character === '\\') {
escaped = true;
} else if (character === '*' || character === '?') {
return true;
}
}

return false;
}

function parseCpeComponents(input, minimumComponentCount) {
const value = normalizeCpeInput(input);
const parts = splitOnUnescapedColon(value);
Expand Down Expand Up @@ -140,7 +156,7 @@ function parseCpeName(input) {
const hasRequiredComponents = REQUIRED_CPE_COMPONENT_INDEXES.every((index) => {
const component = parsed.components[index];
if (index === 0) return CONCRETE_CPE_PART_VALUES.has(component.toLowerCase());
return component && component !== '*';
return component && !hasUnescapedWildcard(component);
});

if (!hasRequiredComponents) {
Expand Down
40 changes: 40 additions & 0 deletions utils/cpe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ describe('CPE helpers', () => {
'cpe:2.3:a:*:product:1.0:*:*:*:*:*:*:*',
'cpe:2.3:a:vendor:*:1.0:*:*:*:*:*:*:*',
'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*',
'cpe:2.3:a:ven*dor:product:1.0',
'cpe:2.3:a:ven?dor:product:1.0',
'cpe:2.3:a:vendor:prod*uct:1.0',
'cpe:2.3:a:vendor:prod?uct:1.0',
...[
'2.6.*',
'2.6*',
'2.*',
'*.*',
'0.*',
'10.*',
'100.*',
'?',
'2.6.?',
'*2.6',
String.raw`2.6\\*`,
String.raw`2.6\\?`,
String.raw`2.6\**`
].map((version) => `cpe:2.3:o:linux:linux_kernel:${version}:*:*:*:*:*:*:*`),
'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*:extra'
])('parseCpeName rejects invalid cpeName value %s', (value) => {
expect(cpe.parseCpeName(value).valid).toBe(false);
Expand All @@ -61,6 +80,17 @@ describe('CPE helpers', () => {
expect(parsed.product).toBe('product\\:edition');
});

test.each([
String.raw`cpe:2.3:a:ven\*dor:prod\?uct:1.0`,
String.raw`cpe:2.3:a:vendor:product:2.6.\*`,
String.raw`cpe:2.3:a:vendor:product:2.6.\\\?`
])('parseCpeName preserves escaped literal wildcard characters in %s', (value) => {
const parsed = cpe.parseCpeName(value);

expect(parsed.valid).toBe(true);
expect(parsed.components.slice(0, 4)).toStrictEqual(value.split(':').slice(2));
});

test('parseVirtualMatchString accepts partial CPE 2.3 match strings without a version component', () => {
const parsed = cpe.parseVirtualMatchString('cpe:2.3:o:linux:linux_kernel');

Expand Down Expand Up @@ -248,6 +278,16 @@ describe('CPE helpers', () => {
expect(cpe.compareCpeVersions(left, right)).toBe(0);
});

test('cpeMatchAppliesToCpeName rejects a wildcard version instead of comparing it as a concrete version', () => {
const criteria = {
criteria: 'cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*',
versionEndExcluding: '5.19'
};

expect(cpe.cpeMatchAppliesToCpeName(criteria, 'cpe:2.3:o:linux:linux_kernel:2.6.*:*:*:*:*:*:*:*')).toBe(false);
expect(cpe.cpeMatchAppliesToCpeName(criteria, 'cpe:2.3:o:linux:linux_kernel:2.6.12:*:*:*:*:*:*:*')).toBe(true);
});

test('cpeMatchAppliesToCpeName honors inclusive and exclusive version boundaries', () => {
const cpeName = 'cpe:2.3:a:vendor:product:2.10:*:*:*:*:*:*:*';

Expand Down
Loading