Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
NODE_CONFIG_ENV=devel
PORT=3000
BASE_URL=
OpenSearchCveIndex=cve-index-local
OpenSearchAllowUnknownSslCerts=false
OpenSearchDomainEndpoint=
Expand All @@ -16,6 +18,9 @@ SearchReadinessTimeoutMs=5000
RequestLogSuccessSampleRate=0.1
RequestLogHealthChecks=false
ShutdownTimeoutMs=35000
HttpHeadersTimeoutMs=10000
HttpRequestTimeoutMs=15000
HttpKeepAliveTimeoutMs=5000
WebSearchEnabled=true
SearchRateLimitEnabled=false
SearchRateLimitWindowMs=60000
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs24-debian13:nonroot

FROM node:${NODE_VERSION} AS deps

ENV NODE_ENV=production
ENV NODE_ENV=production \
NODE_CONFIG_ENV=prod

WORKDIR /usr/src/app

Expand All @@ -18,7 +19,8 @@ RUN npm ci --omit=dev \

FROM ${DISTROLESS_NODE_IMAGE} AS runtime

ENV NODE_ENV=production
ENV NODE_ENV=production \
NODE_CONFIG_ENV=prod

WORKDIR /usr/src/app

Expand Down
6 changes: 4 additions & 2 deletions Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs24-debian13:nonroot

FROM node:${NODE_VERSION} AS deps

ENV NODE_ENV=production
ENV NODE_ENV=production \
NODE_CONFIG_ENV=prod

WORKDIR /usr/src/app

Expand All @@ -23,7 +24,8 @@ RUN --mount=type=secret,id=npm_ca,required=false \

FROM ${DISTROLESS_NODE_IMAGE} AS runtime

ENV NODE_ENV=production
ENV NODE_ENV=production \
NODE_CONFIG_ENV=prod

WORKDIR /usr/src/app

Expand Down
64 changes: 59 additions & 5 deletions README.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions api-docs/openapi.json

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

13 changes: 12 additions & 1 deletion api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ describe('OpenAPI contract', () => {
expect(schema.properties.cveIds.type).toBe('string');
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(
`${constants.MIN_CVE_ID_SEQUENCE_DIGITS}-to-${constants.MAX_CVE_ID_SEQUENCE_DIGITS}-digit sequence number`
);
expect(schema.properties.keywordSearch.description).toContain(
`${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms`
);
Expand All @@ -97,6 +99,8 @@ describe('OpenAPI contract', () => {
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.cpeName.description).toContain('entries independently');
expect(schema.properties.cpeName.description).toContain("does not evaluate a node's operator or negate value");
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);
Expand All @@ -116,6 +120,13 @@ describe('OpenAPI contract', () => {
expect(schema.properties.virtualMatchString.description).toContain('insensitive to lexical case');
expect(schema.properties.virtualMatchString.description).toContain('without a version component');
expect(schema.properties.virtualMatchString.description).toContain('part must be a, h, o, *, or -');
expect(schema.properties.virtualMatchString.description).toContain(
'explicit empty colon-delimited components are invalid'
);
expect(schema.properties.virtualMatchString.description).toContain('entries independently');
expect(schema.properties.virtualMatchString.description).toContain(
"does not evaluate a node's operator or negate value"
);
expect(schema.properties.virtualMatchString.example).toBe('cpe:2.3:o:microsoft:windows_10_1809');
expect(schema.properties.cweId.description).toContain('CNA data');
expect(schema.properties.cweId.description).toContain('ADP-provided CWE data is not searched');
Expand Down
7 changes: 6 additions & 1 deletion config/custom-environment-variables.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
"healthChecks": "RequestLogHealthChecks"
},
"runtime": {
"shutdownTimeoutMs": "ShutdownTimeoutMs"
"shutdownTimeoutMs": "ShutdownTimeoutMs",
"http": {
"headersTimeoutMs": "HttpHeadersTimeoutMs",
"requestTimeoutMs": "HttpRequestTimeoutMs",
"keepAliveTimeoutMs": "HttpKeepAliveTimeoutMs"
}
},
"features": {
"webSearchEnabled": "WebSearchEnabled"
Expand Down
12 changes: 11 additions & 1 deletion config/default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// - Each configuration is mapped using custom-environment-variables.jsonc to enable environment varialbe 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
// Explicit invalid values fail application startup. See README.md for accepted ranges and
// required timeout relationships.
"appConfig": {
// constants for search capability
"search": {
Expand Down Expand Up @@ -45,7 +47,15 @@
},
"runtime": {
// Must remain shorter than the container orchestrator's stop timeout.
"shutdownTimeoutMs": "35000"
"shutdownTimeoutMs": "35000",
"http": {
// Limits how long a client can take to send request headers.
"headersTimeoutMs": "10000",
// Limits how long a client can take to send the complete request, including its body.
"requestTimeoutMs": "15000",
// Limits how long an idle keep-alive connection remains open.
"keepAliveTimeoutMs": "5000"
}
},
// Feature flags used for controlled endpoint migrations.
"features": {
Expand Down
38 changes: 26 additions & 12 deletions config/runtime-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('runtime container and compose configuration', () => {
expect(dockerfile).toContain('ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs24-debian13:nonroot');
expect(dockerfile).toContain('FROM node:${NODE_VERSION} AS deps');
expect(dockerfile).toContain('FROM ${DISTROLESS_NODE_IMAGE} AS runtime');
expect(dockerfile).toContain('NODE_CONFIG_ENV=prod');
expect(dockerfile).toMatch(/^RUN npm ci --omit=dev \\/m);
expect(dockerfile).toMatch(/^COPY --chown=65532:65532 config \.\/config\//m);
expect(dockerfile).toMatch(/^COPY --chown=65532:65532 controllers \.\/controllers\//m);
Expand All @@ -36,6 +37,7 @@ describe('runtime container and compose configuration', () => {
expect(dockerfile).toContain('ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs24-debian13:nonroot');
expect(dockerfile).toContain('FROM node:${NODE_VERSION} AS deps');
expect(dockerfile).toContain('FROM ${DISTROLESS_NODE_IMAGE} AS runtime');
expect(dockerfile).toContain('NODE_CONFIG_ENV=prod');
expect(dockerfile).toMatch(/^RUN --mount=type=secret,id=npm_ca,required=false/m);
expect(dockerfile).toContain('if [ -s /run/secrets/npm_ca ]; then');
expect(dockerfile).toContain('NPM_CONFIG_CAFILE=/run/secrets/npm_ca npm ci --omit=dev');
Expand All @@ -61,6 +63,12 @@ describe('runtime container and compose configuration', () => {
expect(localDockerfile).not.toContain('CMD ["./docker-entrypoint.sh"]');
});

test('local environment example selects the development node-config profile', () => {
const environmentExample = readProjectFile('.env.example');

expect(environmentExample).toContain('NODE_CONFIG_ENV=devel');
});

test('Docker build context excludes tests and local report artifacts from runtime directory copies', () => {
const dockerignore = readProjectFile('.dockerignore');

Expand Down Expand Up @@ -124,6 +132,9 @@ describe('runtime container and compose configuration', () => {
expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"');
expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"');
expect(customEnvironmentVariables).toContain('"shutdownTimeoutMs": "ShutdownTimeoutMs"');
expect(customEnvironmentVariables).toContain('"headersTimeoutMs": "HttpHeadersTimeoutMs"');
expect(customEnvironmentVariables).toContain('"requestTimeoutMs": "HttpRequestTimeoutMs"');
expect(customEnvironmentVariables).toContain('"keepAliveTimeoutMs": "HttpKeepAliveTimeoutMs"');
expect(customEnvironmentVariables).toContain('"webSearchEnabled": "WebSearchEnabled"');
expect(customEnvironmentVariables).toContain('"enabled": "SearchRateLimitEnabled"');
expect(customEnvironmentVariables).toContain('"windowMs": "SearchRateLimitWindowMs"');
Expand All @@ -140,16 +151,19 @@ describe('runtime container and compose configuration', () => {
}
);

test.each([['README.md'], ['config/default.jsonc'], ['config/devel.jsonc'], ['config/prod.jsonc']])(
'%s does not include credential-bearing OpenSearch endpoint examples',
(filePath) => {
const content = readProjectFile(filePath);
const defaultCredentialPair = ['admin', 'admin'].join(':');
const placeholderCredentialPair = ['<user>', '<password>'].join(':');

expect(content).not.toContain(defaultCredentialPair);
expect(content).not.toContain(placeholderCredentialPair);
expect(content).not.toMatch(/https?:\/\/[^/\s]+:[^@\s]+@/);
}
);
test.each([
['README.md'],
['config/default.jsonc'],
['config/devel.jsonc'],
['config/prod.jsonc'],
['config/test.jsonc']
])('%s does not include credential-bearing OpenSearch endpoint examples', (filePath) => {
const content = readProjectFile(filePath);
const defaultCredentialPair = ['admin', 'admin'].join(':');
const placeholderCredentialPair = ['<user>', '<password>'].join(':');

expect(content).not.toContain(defaultCredentialPair);
expect(content).not.toContain(placeholderCredentialPair);
expect(content).not.toMatch(/https?:\/\/[^/\s]+:[^@\s]+@/);
});
});
10 changes: 9 additions & 1 deletion config/test.jsonc
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{
// Tests inherit default configuration. This file exists so node-config recognizes NODE_ENV=test.
// Tests inherit default configuration. These non-routable values satisfy startup validation
// without requiring a live OpenSearch dependency.
"appConfig": {
"search": {
"providerEndpoint": "https://search.example.com",
"index": "test-index",
"allowUnknownSslCerts": "FALSE"
}
}
}
11 changes: 4 additions & 7 deletions controllers/middleware.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const errors = require('../utils/errors');
const constants = require('../utils/constants');
const cpe = require('../utils/cpe');
const cveId = require('../utils/cveId');
const cvss = require('../utils/cvss');
const error = new errors.errors();

Expand Down Expand Up @@ -58,7 +59,6 @@ const nonStringSearchParams = [
const nonEmptyStringParams = constants.SEARCH_QUERY_PARAMS.filter((param) => !nonStringSearchParams.includes(param));

const dateTimeFormat = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/;
const cveIdFormat = /^CVE-\d{4}-\d{4,}$/i;

function isTrueParam(value) {
return value === true || String(value).toLowerCase() === 'true';
Expand All @@ -83,8 +83,8 @@ function countWhitespaceDelimitedTokens(value) {
function parseCveIds(value) {
if (typeof value !== 'string') return null;

const cveIds = value.split(',').map((id) => id.trim());
if (!cveIds.length || cveIds.some((id) => !id || !cveIdFormat.test(id))) return null;
const cveIds = value.split(',').map(cveId.normalizeCveId);
if (!cveIds.length || cveIds.some((id) => !cveId.isValidCveId(id))) return null;

return cveIds;
}
Expand Down Expand Up @@ -152,15 +152,12 @@ function validateParams(req, res, next) {
if (!cveIds) {
return res.status(400).json(error.cveIdsFormatError());
}
if (cveIds.some((cveId) => cveId.length > constants.MAX_CVE_ID_LENGTH)) {
return res.status(400).json(error.cveIdLengthError());
}
if (cveIds.length > constants.MAX_CVE_IDS) {
return res.status(400).json(error.cveIdsLimitError());
}
}

if (hasParam(params, 'noRejected') && ![true, false, ''].includes(params.noRejected)) {
if (hasParam(params, 'noRejected') && ![true, false].includes(params.noRejected)) {
return res.status(400).json(error.noRejectedValueError());
}

Expand Down
Loading
Loading