Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
57bdfa0
feat: define public search response contract
afoote-mitre Sep 10, 2026
8a56e74
feat: warn for pre-1999 CVE IDs
afoote-mitre Sep 10, 2026
778d05a
Merge pull request #153 from CVEProject/af/search-response-contract
jdaigneau5 Sep 10, 2026
ef83d4f
feat: strengthen CPE validation and warnings
afoote-mitre Sep 10, 2026
f831b7c
refactor: remove search performance telemetry
afoote-mitre Sep 10, 2026
c058d47
build(deps): update morgan and js-yaml
afoote-mitre Sep 10, 2026
0d5643f
Merge pull request #155 from CVEProject/af/cpe-validation-public-cleanup
jdaigneau5 Sep 14, 2026
74c4abe
fix: reject embedded wildcards in cpeName searches
afoote-mitre Sep 22, 2026
2527202
Merge pull request #157 from CVEProject/af/cpe-name-wildcard-validation
jdaigneau5 Sep 23, 2026
c998ed8
docs: align API guidance with current behavior
afoote-mitre Sep 23, 2026
fdd8b0f
fix: preserve escaped CPE components in candidate patterns
afoote-mitre Sep 23, 2026
ba3ce45
fix: reject empty stored CPE version intervals
afoote-mitre Sep 23, 2026
b9f48fc
fix: enforce deadlines during semantic filtering
afoote-mitre Sep 23, 2026
f58dba5
fix: reject conflicting field types during readiness checks
afoote-mitre Sep 23, 2026
ec6fca9
fix: honor Postman environment fault-check settings
afoote-mitre Sep 23, 2026
d8cc7fd
fix: sanitize terminal parser and server errors
afoote-mitre Sep 23, 2026
fb69e72
fix: bound configured search timeouts to timer limits
afoote-mitre Sep 23, 2026
20674b6
Merge pull request #158 from CVEProject/af/documentation-refresh
jdaigneau5 Sep 23, 2026
df83ca9
Merge dev and resolve review remediation documentation conflicts
afoote-mitre Sep 28, 2026
b765b9a
Merge pull request #159 from CVEProject/af/review-remediations
jdaigneau5 Sep 30, 2026
786fca7
Merge pull request #160 from CVEProject/dev
jdaigneau5 Oct 1, 2026
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
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ OpenSearchDomainEndpoint=
SearchOpenSearchRequestTimeoutMs=20000
SearchRequestDeadlineMs=30000
SearchReadinessTimeoutMs=5000
SearchLogPerformanceTimings=false
SearchPerformanceTelemetryIntervalMs=10000
RequestLogSuccessSampleRate=0.1
RequestLogHealthChecks=false
WebSearchEnabled=true
Expand Down
98 changes: 64 additions & 34 deletions README.md

Large diffs are not rendered by default.

213 changes: 147 additions & 66 deletions api-docs/openapi.json

Large diffs are not rendered by default.

65 changes: 50 additions & 15 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path');
const openapi = require('./openapi.json');
const packageInfo = require('../package.json');
const constants = require('../utils/constants');
const cpe = require('../utils/cpe');
const { generateOpenApi } = require('../routes/swagger');

function jsonRequestBodySchema(path, method) {
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('OpenAPI contract', () => {
expect(schema.description).toContain('the start must be before or equal to the end');
expect(schema.description).toContain('Direct searches report exact totals');
expect(schema.description).toContain('semantic post-filtering');
expect(schema.description).toContain('10,000 OpenSearch candidates');
expect(schema.description).toContain('10,000 search candidates');
expect(Object.keys(schema.properties)).toStrictEqual(constants.SEARCH_QUERY_PARAMS);
Object.values(schema.properties).forEach((property) => {
expect(property.description).toEqual(expect.any(String));
Expand All @@ -71,6 +72,8 @@ describe('OpenAPI contract', () => {
expect(schema.properties.cveIds.description).toContain('comma-separated list of CVE IDs');
expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_IDS} CVE IDs`);
expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_ID_LENGTH} characters per ID`);
expect(schema.properties.cveIds.description).toContain(`before ${constants.FIRST_CVE_RECORD_YEAR}`);
expect(schema.properties.cveIds.description).toContain('non-fatal response warning');
expect(schema.properties.keywordSearch.description).toContain(
`${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms`
);
Expand Down Expand Up @@ -103,6 +106,8 @@ describe('OpenAPI contract', () => {
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('HTTP 400');
expect(schema.properties.cpeName.description).toContain('CPE_COMPONENTS_DEFAULTED');
expect(schema.properties.cpeName.description).toContain(
'Every concrete non-version component must be compatible with the same CPE Match Criteria entry'
);
Expand Down Expand Up @@ -130,6 +135,8 @@ describe('OpenAPI contract', () => {
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('valid part component');
expect(schema.properties.virtualMatchString.description).toContain('BROAD_VIRTUAL_MATCH_STRING');
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 @@ -168,10 +175,15 @@ describe('OpenAPI contract', () => {
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('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'].description).toBe('Invalid cpeName value');
expect(openapi.paths['/search'].post.responses['404']).toBeUndefined();
expect(openapi.paths['/search'].post.responses['429']).toBeUndefined();
});

Expand All @@ -181,36 +193,59 @@ describe('OpenAPI contract', () => {
expect(responses['400'].description).toContain('Malformed JSON');
expect(responses['415'].description).toContain('application/json');
expect(responses['502'].description).toContain('invalid or unexpected response');
expect(responses['503'].description).toContain('OpenSearch');
expect(responses['503'].description).toContain('search service');
expect(responses['503'].headers['Retry-After']).toStrictEqual({
description: 'Seconds before retrying an unavailable OpenSearch dependency',
description: 'Seconds before retrying the unavailable search service',
schema: {
type: 'integer'
}
});
});

test.each(['/search', '/webSearch'])('documents sanitized parser and server errors for %s', (path) => {
const responses = openapi.paths[path].post.responses;
for (const status of ['413', '415', '500']) {
expect(responses[status].content['application/json'].schema.$ref).toBe('#/components/schemas/ApiError');
}
expect(responses['413'].description).toContain('REQUEST_BODY_TOO_LARGE');
expect(responses['500'].description).toContain('INTERNAL_SERVER_ERROR');
});

test('documents reusable success and error response schemas', () => {
const schemas = openapi.components.schemas;
const searchResponse = schemas.SearchResponse;

expect(searchResponse.required).toStrictEqual([
expect(searchResponse.required).toStrictEqual(['apiVersion', 'status', 'warnings', 'pagination', 'records']);
expect(Object.keys(searchResponse.properties)).toStrictEqual([
'apiVersion',
'status',
'warnings',
'pagination',
'records'
]);
expect(searchResponse.properties.apiVersion.enum).toStrictEqual(['1.0']);
expect(searchResponse.properties.warnings.items.$ref).toBe('#/components/schemas/SearchWarning');
expect(searchResponse.properties.pagination.$ref).toBe('#/components/schemas/SearchPagination');
expect(searchResponse.properties.records.items.$ref).toBe('#/components/schemas/CveRecord');
expect(schemas.SearchWarning.additionalProperties).toBe(false);
expect(schemas.SearchWarning.required).toStrictEqual(['code', 'message']);
expect(schemas.SearchWarning.properties.cveIds.items.type).toBe('string');
expect(schemas.SearchWarning.properties.components.items.enum).toStrictEqual(cpe.CPE_COMPONENT_NAMES);
expect(schemas.SearchPagination.additionalProperties).toBe(false);
expect(schemas.SearchPagination.required).toStrictEqual([
'page',
'pageSize',
'resultsReturned',
'totalResults',
'totalPages',
'status',
'data'
'totalIsExact'
]);
expect(searchResponse.properties.pageSize.description).toContain('requested for this page');
expect(searchResponse.properties.resultsReturned.description).toContain('actually returned');
expect(searchResponse.properties.totalPages.description).toContain('10,000-candidate set');
expect(searchResponse.properties.data.properties.hits.$ref).toBe('#/components/schemas/SearchHits');
expect(schemas.SearchHits.properties.total.properties.value.description).toContain(
expect(schemas.SearchPagination.properties.totalResults.description).toContain(
'exact number of matching records'
);
expect(schemas.SearchHits.properties.total.properties.value.description).toContain('10,000-candidate set');
expect(schemas.SearchHits.properties.hits.items.$ref).toBe('#/components/schemas/SearchHit');
expect(schemas.SearchPagination.properties.totalResults.description).toContain('10,000-candidate set');
expect(schemas.SearchPagination.properties.totalIsExact.description).toContain('additional matches may exist');
expect(schemas.CveRecord.description).toContain('without search-backend transport metadata');
expect(schemas.ApiError.required).toStrictEqual(['error', 'message']);
expect(schemas.ApiErrorResponse.oneOf).toHaveLength(2);
expect(openapi.paths['/search'].post.responses['200'].content['application/json'].schema.$ref).toBe(
Expand Down
6 changes: 2 additions & 4 deletions config/custom-environment-variables.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
"search": {
"providerEndpoint": "OpenSearchDomainEndpoint",
"index": "OpenSearchCveIndex",
// allows local development using containers that do not have SSL certs
// TRUE disables OpenSearch server certificate verification; isolated local development only.
"allowUnknownSslCerts": "OpenSearchAllowUnknownSslCerts",
"openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs",
"requestDeadlineMs": "SearchRequestDeadlineMs",
"readinessTimeoutMs": "SearchReadinessTimeoutMs",
"performanceTelemetryEnabled": "SearchLogPerformanceTimings",
"performanceTelemetryIntervalMs": "SearchPerformanceTelemetryIntervalMs"
"readinessTimeoutMs": "SearchReadinessTimeoutMs"
},
"requestLogging": {
"successSampleRate": "RequestLogSuccessSampleRate",
Expand Down
19 changes: 7 additions & 12 deletions config/default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,27 @@
// Default configuration
// These values are overridable using other *.jsonc (e.g., prod.jsonc)
// as well as using environment variables (e.g., in `.env`)
// - Each configuration is mapped using custom-environment-variables.jsonc to enable environment varialbe overrides.
// - Each configuration is mapped using custom-environment-variables.jsonc to enable environment variable overrides.
// - For more information, see cve-core/src/adapters/config/AppConfig.ts
// NOTE for consistency, all values need to be strings for proper type when using .env overrides
"appConfig": {
// constants for search capability
"search": {
// setting this to FALSE (recommended) requires an SSL cert to access the search server
// The only time this should be allowed to be true is when developing or testing
// using containers that do not have SSL certs
// FALSE (recommended) verifies the OpenSearch server's TLS certificate.
// TRUE disables verification; use only for isolated local development with self-signed certificates.
"allowUnknownSslCerts": "FALSE",
// Limits each request from the API process to OpenSearch.
"openSearchRequestTimeoutMs": "20000",
// Limits total processing time for a /search request.
"requestDeadlineMs": "30000",
// Limits the minimal OpenSearch request used by /api/ready-check.
"readinessTimeoutMs": "5000",
// Emits aggregate search-stage timing summaries for controlled diagnostics.
"performanceTelemetryEnabled": "FALSE",
// Controls how often enabled performance summaries are emitted.
"performanceTelemetryIntervalMs": "10000"
// Overall /api/ready-check deadline and per-operation timeout for field capabilities and search.
"readinessTimeoutMs": "5000"
},
// Access logging settings. Error responses are always logged.
"requestLogging": {
// Fraction of successful non-health-check requests to log, from 0 through 1.
// Fraction of successful requests to log, including health checks when enabled, from 0 through 1.
"successSampleRate": "0.1",
// Set to TRUE to retain successful health-check access logs.
// Set to TRUE to include successful health checks in the same successSampleRate sampling.
"healthChecks": "FALSE"
},
// Feature flags used for operator-controlled endpoint availability.
Expand Down
5 changes: 2 additions & 3 deletions config/devel.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
"providerEndpoint": "https://localhost:9200",
// index on search server related to searching CVEs
"index": "e2e-cve-test-index-1109",
// setting this to FALSE (recommended) requires an SSL cert to access the search server
// The only time this should be allowed to be true is when developing or testing
// using containers that do not have SSL certs
// FALSE verifies the OpenSearch server's TLS certificate.
// TRUE disables verification for isolated local development with self-signed certificates.
// DO NOT USE THIS IN ANY PUBLIC OR PRODUCTION ENVIRONMENTS
"allowUnknownSslCerts": "TRUE"
},
Expand Down
4 changes: 0 additions & 4 deletions config/runtime-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ describe('runtime container and compose configuration', () => {
);
expect(customEnvironmentVariables).toContain('"requestDeadlineMs": "SearchRequestDeadlineMs"');
expect(customEnvironmentVariables).toContain('"readinessTimeoutMs": "SearchReadinessTimeoutMs"');
expect(customEnvironmentVariables).toContain('"performanceTelemetryEnabled": "SearchLogPerformanceTimings"');
expect(customEnvironmentVariables).toContain(
'"performanceTelemetryIntervalMs": "SearchPerformanceTelemetryIntervalMs"'
);
expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"');
expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"');
expect(customEnvironmentVariables).toContain('"webSearchEnabled": "WebSearchEnabled"');
Expand Down
31 changes: 13 additions & 18 deletions controllers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ function validateParams(req, res, next) {
}
}

const parsedCpeName = params.cpeName ? cpe.parseCpeName(params.cpeName) : null;
if (parsedCpeName && !parsedCpeName.valid) {
return res.status(400).json(error.invalidCpeNameError(parsedCpeName.reason));
}

const parsedVirtualMatchString = params.virtualMatchString
? cpe.parseVirtualMatchString(params.virtualMatchString)
: null;
if (parsedVirtualMatchString && !parsedVirtualMatchString.valid) {
return res.status(400).json(error.virtualMatchStringFormatError(parsedVirtualMatchString.reason));
}

if (
typeof params.referenceURL === 'string' &&
params.referenceURL.trim().length < constants.MIN_REFERENCE_URL_LENGTH
Expand Down Expand Up @@ -273,12 +285,7 @@ function validateParams(req, res, next) {
}
}

const virtualMatchString = cpe.parseVirtualMatchString(params.virtualMatchString);
if (!virtualMatchString.valid) {
return res.status(400).json(error.virtualMatchStringVersionRangeFormatError());
}

if (virtualMatchString.hasVersionComponent) {
if (parsedVirtualMatchString.hasVersionComponent) {
return res.status(400).json(error.virtualMatchStringVersionComponentError());
}

Expand Down Expand Up @@ -326,23 +333,11 @@ function validateParams(req, res, next) {
return res.status(400).json(error.virtualMatchVulnError());
}

if (params?.virtualMatchString) {
const virtualMatchString = cpe.parseVirtualMatchString(params.virtualMatchString);
if (!virtualMatchString.valid) {
return res.status(400).json(error.virtualMatchStringFormatError());
}
}

//Ensures cpeName is specified if isVulnerable is true
if (isTrueParam(params?.isVulnerable) && !params.cpeName) {
return res.status(400).json(error.missingCpeNameError());
}

//Ensures cpeName contains the required concrete CPE components
if (params?.cpeName && !cpe.isValidCpeName(params.cpeName)) {
return res.status(404).json(error.invalidCpeNameError());
}

//Ensures that resultsPerPage is a positive integer
if (
hasParam(params, 'resultsPerPage') &&
Expand Down
Loading
Loading