Skip to content

Commit

Permalink
Improve engagements and findings retrieval
Browse files Browse the repository at this point in the history
- Fetch all product engagements by default
- Fetch findings by engagements
- Improve the HTML report
- Update dependencies
  • Loading branch information
GaelGirodon committed Jan 4, 2024
1 parent b705219 commit 2d24180
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 60 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"src/**/*.ejs"
],
"dependencies": {
"axios": "^1.6.2",
"axios": "^1.6.4",
"ejs": "npm:neat-ejs@^3.1.9",
"jsonpath-plus": "^7.2.0"
},
Expand Down
49 changes: 23 additions & 26 deletions src/defectdojo.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export class DefectDojoApiClient {
throw new Error("expected to find a single product");
}
const product = results[0];
product.title = product.description || product.name;
const d = product.description?.trim();
product.title = d && d.length < 60 && !d.includes("\n") ? d : product.name;
product.url = `${this.url}/product/${product.id}`;
console.log(`[info] Product id = ${product.id}`);
return product;
Expand All @@ -50,49 +51,45 @@ export class DefectDojoApiClient {
}

/**
* Fetch an engagement by product and by name.
* Fetch engagements by product and name.
*
* @param {string} productId Product id
* @param {string} name Engagement name
* @returns The engagement
* @param {string} name Engagement name (optional)
* @returns Engagements
* @throws Request error
*/
async getEngagement(productId, name) {
console.log(`[info] Fetching engagement '${name}' for product id '${productId}'`);
async getEngagements(productId, name) {
console.log(`[info] Fetching engagement${name ? ` '${name}'` : 's'} for product id '${productId}'`);
try {
const response = await this.http.get(`/engagements?product=${productId}&name=${name}`);
const results = response.data?.results?.filter(e => e.name === name); // Exact match
if (results?.length !== 1) {
throw new Error("expected to find a single engagement");
}
const engagement = results[0];
engagement.url = `${this.url}/engagement/${engagement.id}`;
console.log(`[info] Engagement id = ${engagement.id}`);
return engagement;
const query = [];
query.push(`product=${productId}`);
if (name) query.push(`name=${name}`);
query.push("o=-updated", "limit=100");
const response = await this.http.get("/engagements?" + query.join("&"));
const engagements = response.data?.results
?.filter(e => !name || e.name === name) // Exact match
?.map(e => ({ ...e, url: `${this.url}/engagement/${e.id}` }))
?? [];
console.log(`[info] Engagements count = ${engagements.length}`);
return engagements;
} catch (error) {
throw new Error(`An error occurred fetching engagements: ${error?.message ?? error}`);
}
}

/**
* Fetch vulnerabilities associated to one or multiple products
* and engagements.
* Fetch vulnerabilities associated to one or multiple engagements.
*
* @param {string[]} products Products ids
* @param {string[]} engagements Engagements ids (optional)
* @param {string[]} engagements Engagements ids
* @param {string[]} statuses Statuses to filter
* @returns Vulnerabilities
* @throws Request error
*/
async getFindings(products, engagements, statuses) {
console.log(`[info] Fetching findings for product(s) [${products.join(", ")}]`
+ ` and engagement(s) [${engagements.join(", ")}]`);
async getFindings(engagements, statuses) {
console.log(`[info] Fetching findings for engagement(s) [${engagements.join(", ")}]`);
try {
const query = [];
query.push(`test__engagement__product=${products.join(",")}`);
if (engagements?.length > 0) {
query.push(`test__engagement=${engagements.join(",")}`);
}
query.push(`test__engagement=${engagements.join(",")}`);
query.push(...statuses.map(s => s[0] !== "!" ? s + "=true" : s.slice(1) + "=false"));
query.push("limit=100", "related_fields=true");
let findingsUrl = "/findings?" + query.join("&");
Expand Down
16 changes: 8 additions & 8 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ export async function main() {
}, []);

// Fetch engagements
const engagements = !opts.engagement ? [] :
await products.reduce(async (prevResults, p) => {
const results = await prevResults;
const engagement = await defectDojo.getEngagement(p.id, opts.engagement)
.catch((e) => { console.error(`[error] ${e.message}`); process.exit(1); });
return [...results, engagement];
}, []);
const engagements = await products.reduce(async (prevResults, p) => {
const results = await prevResults;
const engagements = await defectDojo.getEngagements(p.id, opts.engagement)
.catch((e) => { console.error(`[error] ${e.message}`); process.exit(1); });
p.engagements = engagements;
return [...results, ...engagements];
}, []);

// Fetch vulnerabilities
const findings = await defectDojo
.getFindings(products.map(p => p.id), engagements.map(e => e.id), opts.status)
.getFindings(engagements.map(e => e.id), opts.status)
.catch((e) => { console.error(`[error] ${e.message}`); process.exit(1); });

/*
Expand Down
14 changes: 8 additions & 6 deletions src/template.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,16 @@
<img class="logo" src="<%= config.logo %>" alt="Company logo">
<%_ } -%>
<h1><%= config.title %></h1>
<%_ for (let i = 0; i < products.length && i < engagements.length; i++) { -%>
<%_ for (const product of products) { -%>
<h2>
<span><%= products[i].title %></span>
<%_ if (engagements[i].version) { -%>
<span class="tag" title="Application version"><%= engagements[i].version %></span>
<span><%= product.title %></span>
<%_ if (product.engagements?.length > 0) { -%>
<%_ if (engagements[0].version) { -%>
<span class="tag" title="Application version"><%= engagements[0].version %></span>
<%_ } -%>
<%_ if (engagements[0].updated) { -%>
<span class="tag" title="Last security debt update"><%= engagements[0].updated.substring(0, 10) %></span>
<%_ } -%>
<%_ if (engagements[i].updated) { -%>
<span class="tag" title="Last security debt update"><%= engagements[i].updated.substring(0, 10) %></span>
<%_ } -%>
</h2>
<%_ } -%>
Expand Down
31 changes: 22 additions & 9 deletions test/defectdojo.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,35 @@ describe("DefectDojoApiClient", function () {
});
});

describe("#getEngagement()", function () {
it("should return a single engagement", async function () {
let engagement;
await assert.doesNotReject(async () => engagement = await client.getEngagement(1, "main"));
assert.strictEqual(engagement.id, 1);
assert.strictEqual(engagement.url, "http://localhost:8888/engagement/1");
describe("#getEngagements()", function () {
it("should return all product engagements when searching by product id", async function () {
let engagements;
await assert.doesNotReject(async () => engagements = await client.getEngagements(1));
assert.strictEqual(engagements.length, 2);
assert.strictEqual(engagements[0].id, 1);
assert.strictEqual(engagements[0].url, "http://localhost:8888/engagement/1");
assert.strictEqual(engagements[1].id, 2);
assert.strictEqual(engagements[1].url, "http://localhost:8888/engagement/2");
});
it("should throw if the engagement doesn't exist", async function () {
await assert.rejects(() => client.getEngagement(1, "unknown"));
it("should return a single engagement when searching also by name", async function () {
let engagements;
await assert.doesNotReject(async () => engagements = await client.getEngagements(1, "main"));
assert.strictEqual(engagements.length, 1);
assert.strictEqual(engagements[0].id, 1);
assert.strictEqual(engagements[0].name, "main");
assert.strictEqual(engagements[0].url, "http://localhost:8888/engagement/1");
});
it("should return no engagement if none exists with the given name", async function () {
let engagements;
await assert.doesNotReject(async () => engagements = await client.getEngagements(1, "unknown"));
assert.strictEqual(engagements.length, 0);
});
});

describe("#getFindings()", function () {
it("should return findings", async function () {
let findings;
await assert.doesNotReject(async () => findings = await client.getFindings([], [], []));
await assert.doesNotReject(async () => findings = await client.getFindings([], []));
assert.strictEqual(10, findings.length);
});
it("should throw if something went wrong", async function () {
Expand Down
3 changes: 2 additions & 1 deletion test/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ app.get("/api/v2/products", (req, res) => {
// GET /api/v2/engagements should return a single test engagement.
app.get("/api/v2/engagements", (req, res) => {
const results = [
{ id: 1, name: "main" }
{ id: 1, name: "main", version: "1.1.0", updated: "2024-01-01" },
{ id: 2, name: "old", version: "1.0.0", updated: "2023-01-01" }
].filter(e => !req.params.name || e.name.includes(req.params.name));
res.json({ count: results.length, results });
});
Expand Down

0 comments on commit 2d24180

Please sign in to comment.