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
37 changes: 24 additions & 13 deletions src/analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { addProxyAgent, getCustom, getTokenHeaders , TRUSTIFY_DA_OPERATION_TYPE_
/** Media type for CycloneDX JSON batch payloads (batch-analysis API). */
export const CYCLONEDX_JSON_MEDIA_TYPE = 'application/vnd.cyclonedx+json'

export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken }
export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken, appendAnalysisQueryParams }

/**
* Send a stack analysis request and get the report as 'text/html' or 'application/json'.
Expand Down Expand Up @@ -47,9 +47,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
}, opts);

const finalUrl = new URL(`${url}/api/v5/analysis`);
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
finalUrl.searchParams.append('recommend', 'false');
}
appendAnalysisQueryParams(finalUrl, opts);

let resp = await fetch(finalUrl, fetchOptions)
let result
Expand Down Expand Up @@ -112,9 +110,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
}, opts);

const finalUrl = new URL(`${url}/api/v5/analysis`);
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
finalUrl.searchParams.append('recommend', 'false');
}
appendAnalysisQueryParams(finalUrl, opts);

let resp = await fetch(finalUrl, fetchOptions)
let result
Expand Down Expand Up @@ -156,9 +152,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
*/
async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
const finalUrl = new URL(`${url}/api/v5/batch-analysis`)
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
finalUrl.searchParams.append('recommend', 'false')
}
appendAnalysisQueryParams(finalUrl, opts);

const fetchOptions = addProxyAgent({
method: 'POST',
Expand Down Expand Up @@ -209,9 +203,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
}

const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
finalUrl.searchParams.append('recommend', 'false');
}
appendAnalysisQueryParams(finalUrl, opts);

const fetchOptions = addProxyAgent({
method: 'POST',
Expand Down Expand Up @@ -247,6 +239,25 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
}
}

/**
* Append common analysis query parameters (recommend, providers, sources) to a URL.
* @param {URL} url - the URL object to append query parameters to
* @param {import("index.js").Options} [opts={}] - options containing parameter overrides
*/
function appendAnalysisQueryParams(url, opts = {}) {
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
url.searchParams.append('recommend', 'false');
}
const providers = getCustom('TRUSTIFY_DA_PROVIDERS', null, opts);
if (providers) {
url.searchParams.append('providers', providers);
}
const sources = getCustom('TRUSTIFY_DA_SOURCES', null, opts);
if (sources) {
url.searchParams.append('sources', sources);
}
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

/**
*
* @param url the backend url to send the request to
Expand Down
59 changes: 58 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,25 @@ const component = {
desc: 'Workspace root directory (for monorepos; lock file is expected here)',
type: 'string',
normalize: true,
},
providers: {
desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
type: 'string',
},
sources: {
desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
type: 'string',
}
}),
handler: async args => {
let manifestName = args['/path/to/manifest']
const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {}
if (args.providers) {
opts.TRUSTIFY_DA_PROVIDERS = args.providers
}
if (args.sources) {
opts.TRUSTIFY_DA_SOURCES = args.sources
}
let res = await client.componentAnalysis(manifestName, opts)
console.log(JSON.stringify(res, null, 2))
}
Expand Down Expand Up @@ -88,6 +102,14 @@ const image = {
desc: 'For JSON report, get only the \'summary\'',
type: 'boolean',
conflicts: 'html'
},
providers: {
desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
type: 'string',
},
sources: {
desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
type: 'string',
}
}),
handler: async args => {
Expand All @@ -97,7 +119,14 @@ const image = {
}
let html = args['html']
let summary = args['summary']
let res = await client.imageAnalysis(imageRefs, html)
const opts = {}
if (args.providers) {
opts.TRUSTIFY_DA_PROVIDERS = args.providers
}
if (args.sources) {
opts.TRUSTIFY_DA_SOURCES = args.sources
}
let res = await client.imageAnalysis(imageRefs, html, opts)
if(summary && !html) {
let summaries = {}
for (let [imageRef, report] of Object.entries(res)) {
Expand Down Expand Up @@ -152,13 +181,27 @@ const stack = {
desc: 'Workspace root directory (for monorepos; lock file is expected here)',
type: 'string',
normalize: true,
},
providers: {
desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
type: 'string',
},
sources: {
desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
type: 'string',
}
}),
handler: async args => {
let manifest = args['/path/to/manifest']
let html = args['html']
let summary = args['summary']
const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {}
if (args.providers) {
opts.TRUSTIFY_DA_PROVIDERS = args.providers
}
if (args.sources) {
opts.TRUSTIFY_DA_SOURCES = args.sources
}
let theProvidersSummary = new Map();
let theProvidersObject ={}
let res = await client.stackAnalysis(manifest, html, opts)
Expand Down Expand Up @@ -230,6 +273,14 @@ const stackBatch = {
desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
type: 'boolean',
default: false,
},
providers: {
desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
type: 'string',
},
sources: {
desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
type: 'string',
}
}),
handler: async args => {
Expand All @@ -250,6 +301,12 @@ const stackBatch = {
if (args.failFast) {
opts.continueOnError = false
}
if (args.providers) {
opts.TRUSTIFY_DA_PROVIDERS = args.providers
}
if (args.sources) {
opts.TRUSTIFY_DA_SOURCES = args.sources
}
let res = await client.stackAnalysisBatch(workspaceRoot, html, opts)
const batchAnalysis =
res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import path from "node:path";
import { EOL } from "os";

Check warning on line 2 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

There should be at least one empty line between import groups

Check warning on line 2 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

There should be at least one empty line between import groups
import pLimit from 'p-limit'

import { availableProviders, match } from './provider.js'
import analysis from './analysis.js'

Check warning on line 6 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`./analysis.js` import should occur before import of `./provider.js`

Check warning on line 6 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

There should be at least one empty line between import groups

Check warning on line 6 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`./analysis.js` import should occur before import of `./provider.js`

Check warning on line 6 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

There should be at least one empty line between import groups
import fs from 'node:fs'

Check warning on line 7 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`node:fs` import should occur before import of `node:path`

Check warning on line 7 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

There should be at least one empty line between import groups

Check warning on line 7 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`node:fs` import should occur before import of `node:path`

Check warning on line 7 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

There should be at least one empty line between import groups
import { getCustom } from "./tools.js";
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'

Check warning on line 9 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`./batch_opts.js` import should occur before import of `./provider.js`

Check warning on line 9 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`./batch_opts.js` import should occur before import of `./provider.js`
import { discoverMavenModules } from './providers/java_maven.js'

Check warning on line 10 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`./providers/java_maven.js` import should occur before import of `./tools.js`

Check warning on line 10 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`./providers/java_maven.js` import should occur before import of `./tools.js`
import { discoverGradleSubprojects } from './providers/java_gradle.js'

Check warning on line 11 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`./providers/java_gradle.js` import should occur before import of `./tools.js`

Check warning on line 11 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`./providers/java_gradle.js` import should occur before import of `./tools.js`
import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js'

Check warning on line 12 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`./providers/golang_gomodules.js` import should occur before import of `./tools.js`

Check warning on line 12 in src/index.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`./providers/golang_gomodules.js` import should occur before import of `./tools.js`
import { discoverUvWorkspaceMembers } from './providers/python_uv.js'
import {
discoverWorkspaceCrates,
Expand Down Expand Up @@ -61,7 +61,9 @@
* TRUSTIFY_DA_PYTHON_PATH?: string | undefined,
* TRUSTIFY_DA_PYTHON_VIRTUAL_ENV?: string | undefined,
* TRUSTIFY_DA_PYTHON3_PATH?: string | undefined,
* TRUSTIFY_DA_PROVIDERS?: string | undefined,
* TRUSTIFY_DA_RECOMMEND?: string | undefined,
* TRUSTIFY_DA_SOURCES?: string | undefined,
* TRUSTIFY_DA_SKOPEO_CONFIG_PATH?: string | undefined,
* TRUSTIFY_DA_SKOPEO_PATH?: string | undefined,
* TRUSTIFY_DA_SYFT_CONFIG_PATH?: string | undefined,
Expand Down
134 changes: 134 additions & 0 deletions test/analysis.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,140 @@ suite('testing the analysis module for sending api requests', () => {
))
})

suite('appendAnalysisQueryParams — URL construction with providers and sources', () => {
/** Verifies that providers param is appended when set via opts. */
test('appends providers query param from opts', () => {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, { TRUSTIFY_DA_PROVIDERS: 'redhat,lightwell' })
expect(url.searchParams.get('providers')).to.equal('redhat,lightwell')
})

/** Verifies that sources param is appended when set via opts. */
test('appends sources query param from opts', () => {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, { TRUSTIFY_DA_SOURCES: 'osv' })
expect(url.searchParams.get('sources')).to.equal('osv')
})

/** Verifies that both providers and sources are appended together. */
test('appends both providers and sources when both are set', () => {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, {
TRUSTIFY_DA_PROVIDERS: 'redhat',
TRUSTIFY_DA_SOURCES: 'osv,nvd'
})
expect(url.searchParams.get('providers')).to.equal('redhat')
expect(url.searchParams.get('sources')).to.equal('osv,nvd')
})

/** Verifies that no providers or sources params are added when neither is set. */
test('does not append providers or sources when neither is set', () => {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, {})
expect(url.searchParams.has('providers')).to.equal(false)
expect(url.searchParams.has('sources')).to.equal(false)
Comment thread
ruromero marked this conversation as resolved.
})

/** Verifies that environment variables are respected as fallback for providers. */
test('reads providers from environment variable when opts is empty', () => {
process.env['TRUSTIFY_DA_PROVIDERS'] = 'trustify'
try {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, {})
expect(url.searchParams.get('providers')).to.equal('trustify')
} finally {
delete process.env['TRUSTIFY_DA_PROVIDERS']
}
})

/** Verifies that environment variables are respected as fallback for sources. */
test('reads sources from environment variable when opts is empty', () => {
process.env['TRUSTIFY_DA_SOURCES'] = 'osv'
try {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, {})
expect(url.searchParams.get('sources')).to.equal('osv')
} finally {
delete process.env['TRUSTIFY_DA_SOURCES']
}
})

/** Verifies that recommend=false is still appended alongside providers/sources. */
test('appends recommend=false alongside providers and sources', () => {
const url = new URL('http://example.com/api/v5/analysis')
analysis.appendAnalysisQueryParams(url, {
TRUSTIFY_DA_RECOMMEND: 'false',
TRUSTIFY_DA_PROVIDERS: 'redhat',
TRUSTIFY_DA_SOURCES: 'osv'
})
expect(url.searchParams.get('recommend')).to.equal('false')
expect(url.searchParams.get('providers')).to.equal('redhat')
expect(url.searchParams.get('sources')).to.equal('osv')
})
})

suite('verify providers and sources are forwarded in HTTP requests', () => {
let fakeManifest = 'fake-file-providers.typ'
let stackProviderStub = stub()
let fakeProvided = {
ecosystem: 'dummy-ecosystem',
content: 'dummy-content',
contentType: 'dummy-content-type'
}
stackProviderStub.withArgs(fakeManifest).returns(fakeProvided)
let fakeProvider = {
provideComponent: () => { },
provideStack: stackProviderStub,
isSupported: () => { }
}

setup(() => {
fs.writeFileSync(fakeManifest, 'dummy-content')
})

teardown(() => {
if (fs.existsSync(fakeManifest)) {
fs.unlinkSync(fakeManifest)
}
})

/** Verifies that providers query param reaches the backend URL during stack analysis. */
test('requestStack sends providers query param to backend', interceptAndRun(
http.post(`${backendUrl}/api/v5/analysis`, ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('providers') === 'redhat,lightwell') {
return HttpResponse.json({ filtered: true })
}
return new HttpResponse(null, { status: 400 })
}),
async () => {
let res = await analysis.requestStack(
fakeProvider, fakeManifest, backendUrl, false,
{ TRUSTIFY_DA_PROVIDERS: 'redhat,lightwell' }
)
expect(res).to.deep.equal({ filtered: true })
}
))

/** Verifies that sources query param reaches the backend URL during stack analysis. */
test('requestStack sends sources query param to backend', interceptAndRun(
http.post(`${backendUrl}/api/v5/analysis`, ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('sources') === 'osv') {
return HttpResponse.json({ sourceFiltered: true })
}
return new HttpResponse(null, { status: 400 })
}),
async () => {
let res = await analysis.requestStack(
fakeProvider, fakeManifest, backendUrl, false,
{ TRUSTIFY_DA_SOURCES: 'osv' }
)
expect(res).to.deep.equal({ sourceFiltered: true })
}
))
})

suite('addProxyAgent', () => {
afterEach(() => {
delete process.env['TRUSTIFY_DA_PROXY_URL']
Expand Down
Loading