diff --git a/src/action-helper.test.ts b/src/action-helper.test.ts new file mode 100644 index 0000000..9255d8f --- /dev/null +++ b/src/action-helper.test.ts @@ -0,0 +1,103 @@ +import { createMessage } from "./action-helper"; +import { SiteFixture } from "./models/SiteFixture"; +import { AlertFixture } from "./models/AlertFixture"; +import { InstanceFixture } from "./models/InstanceFixture"; + +describe("action-helper", () => { + describe("createMessage", () => { + it("returns the snapshot message", () => { + expect( + createMessage( + [ + { + "@name": "http://localhost:8080", + alerts: [ + { + name: "Cross Site Scripting (Reflected)", + pluginid: "40012", + riskcode: "2", + confidence: "3", + instances: [ + { uri: "http://localhost:8080/bodgeit/contact.jsp" }, + ], + }, + ], + }, + ], + + "2343454356", + "https://github.com/zaproxy/actions-common/actions/runs/4926339347" + ) + ).toMatchInlineSnapshot(` + "- Site: [http://localhost:8080](http://localhost:8080) + **New Alerts** + - **Medium risk (Confidence: High): Cross Site Scripting (Reflected)** [[40012]](https://www.zaproxy.org/docs/alerts/40012) total: 1: + - [http://localhost:8080/bodgeit/contact.jsp](http://localhost:8080/bodgeit/contact.jsp) + + + + https://github.com/zaproxy/actions-common/actions/runs/4926339347 + 2343454356" + `); + }); + + it("returns the count of instances for an alert", () => { + expect( + createMessage( + [ + new SiteFixture({ + alerts: [ + new AlertFixture({ instances: [new InstanceFixture()] }), + ], + }), + ], + "2343454356", + "https://github.com/zaproxy/actions-common/actions/runs/4926339347" + ) + ).toContain("total: 1"); + }); + + it("shows the risk in front of the alert", () => { + expect( + createMessage( + [ + new SiteFixture({ + alerts: [ + new AlertFixture({ + instances: [new InstanceFixture()], + riskcode: "3", + confidence: "2", + }), + ], + }), + ], + "2343454356", + "https://github.com/zaproxy/actions-common/actions/runs/4926339347" + ) + ).toContain("High risk (Confidence: Medium):"); + }); + + it("shows a link to the risk description", () => { + expect( + createMessage( + [ + new SiteFixture({ + alerts: [ + new AlertFixture({ + instances: [new InstanceFixture()], + pluginid: "40012", + }), + ], + }), + ], + "2343454356", + "https://github.com/zaproxy/actions-common/actions/runs/4926339347" + ) + ).toContain("https://www.zaproxy.org/docs/alerts/40012"); + }); + + it("returns an empty string if there are no sites", () => { + expect(createMessage([], "runnerId", "runnerLink")).toBe(""); + }); + }); +}); diff --git a/src/action-helper.ts b/src/action-helper.ts index ab0554b..9775448 100644 --- a/src/action-helper.ts +++ b/src/action-helper.ts @@ -21,309 +21,321 @@ function createReadStreamSafe(filename: string): Promise { }); } -const actionHelper = { - getRunnerID: (body: string): string | null => { - const results = body.match("RunnerID:\\d+"); - if (results !== null && results.length !== 0) { - return results[0].split(":")[1]; - } - return null; - }, - - processLineByLine: async (tsvFile: string) => { - const plugins = []; - try { - const fileStream = await createReadStreamSafe(tsvFile); - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity, - }); - for await (const line of rl) { - if (line.charAt(0) !== "#") { - const tmp = line.split("\t"); - if ( - tmp[0].trim() !== "" && - tmp[1].trim().toUpperCase() === "IGNORE" - ) { - plugins.push(tmp[0].trim()); - } +const getRunnerID = (body: string): string | null => { + const results = body.match("RunnerID:\\d+"); + if (results !== null && results.length !== 0) { + return results[0].split(":")[1]; + } + return null; +}; +const processLineByLine = async (tsvFile: string) => { + const plugins = []; + try { + const fileStream = await createReadStreamSafe(tsvFile); + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity, + }); + for await (const line of rl) { + if (line.charAt(0) !== "#") { + const tmp = line.split("\t"); + if (tmp[0].trim() !== "" && tmp[1].trim().toUpperCase() === "IGNORE") { + plugins.push(tmp[0].trim()); } } - } catch (err) { - console.log(`Error when reading the rules file: ${tsvFile}`); } + } catch (err) { + console.log(`Error when reading the rules file: ${tsvFile}`); + } - return plugins; - }, + return plugins; +}; - createMessage: ( - sites: Site[] | FilteredSite[] | DifferenceSite[], - runnerID: string, - runnerLink: string - ) => { - const NXT_LINE = "\n"; - const TAB = "\t"; - const BULLET = "-"; - let msg = ""; - const instanceCount = 5; +const descriptionsForRisk = { + "0": "Informational", + "1": "Low", + "2": "Medium", + "3": "High", +}; - sites.forEach((site) => { - msg = - msg + - `${BULLET} Site: [${site["@name"]}](${site["@name"]}) ${NXT_LINE}`; - if ("alerts" in site) { - if (site.alerts!.length !== 0) { - msg = `${msg} ${TAB} **New Alerts** ${NXT_LINE}`; - site.alerts!.forEach((alert) => { - msg = - msg + - TAB + - `${BULLET} **${alert.name}** [${alert.pluginid}] total: ${alert.instances.length}: ${NXT_LINE}`; +const descriptionsForConfidence = { + "0": "False Positive", + "1": "Low", + "2": "Medium", + "3": "High", + "4": "Confirmed", +}; - for (let i = 0; i < alert["instances"].length; i++) { - if (i >= instanceCount) { - msg = msg + TAB + TAB + `${BULLET} .. ${NXT_LINE}`; - break; - } - const instance = alert["instances"][i]; - msg = - msg + - TAB + - TAB + - `${BULLET} [${instance.uri}](${instance.uri}) ${NXT_LINE}`; - } - }); - msg = msg + NXT_LINE; - } - } +export const createMessage = ( + sites: Site[] | FilteredSite[] | DifferenceSite[], + runnerID: string, + runnerLink: string +) => { + const NXT_LINE = "\n"; + const TAB = "\t"; + const BULLET = "-"; + let msg = ""; + const instanceCount = 5; + + sites.forEach((site) => { + msg = + msg + `${BULLET} Site: [${site["@name"]}](${site["@name"]}) ${NXT_LINE}`; + if ("alerts" in site) { + if (site.alerts!.length !== 0) { + msg = `${msg} ${TAB} **New Alerts** ${NXT_LINE}`; + site.alerts!.forEach((alert) => { + const risk = descriptionsForRisk[alert.riskcode]; + const confidence = descriptionsForConfidence[alert.confidence]; + msg = + msg + + TAB + + `${BULLET} **${risk} risk (Confidence: ${confidence}): ${alert.name}** [[${alert.pluginid}]](https://www.zaproxy.org/docs/alerts/${alert.pluginid}) total: ${alert.instances.length}: ${NXT_LINE}`; - if (isDifferenceSite(site)) { - if (site.removedAlerts.length !== 0) { - msg = `${msg} ${TAB} **Resolved Alerts** ${NXT_LINE}`; - site.removedAlerts.forEach((alert) => { + for (let i = 0; i < alert["instances"].length; i++) { + if (i >= instanceCount) { + msg = msg + TAB + TAB + `${BULLET} .. ${NXT_LINE}`; + break; + } + const instance = alert["instances"][i]; msg = msg + TAB + - `${BULLET} **${alert.name}** [${alert.pluginid}] total: ${alert.instances.length}: ${NXT_LINE}`; - }); - msg = msg + NXT_LINE; - } + TAB + + `${BULLET} [${instance.uri}](${instance.uri}) ${NXT_LINE}`; + } + }); + msg = msg + NXT_LINE; } + } - if (isFilteredSite(site)) { - if (site.ignoredAlerts.length !== 0) { - msg = `${msg} ${TAB} **Ignored Alerts** ${NXT_LINE}`; - site.ignoredAlerts.forEach((alert) => { - msg = - msg + - TAB + - `${BULLET} **${alert.name}** [${alert.pluginid}] total: ${alert.instances.length}: ${NXT_LINE}`; - }); - msg = msg + NXT_LINE; - } + if (isDifferenceSite(site)) { + if (site.removedAlerts.length !== 0) { + msg = `${msg} ${TAB} **Resolved Alerts** ${NXT_LINE}`; + site.removedAlerts.forEach((alert) => { + msg = + msg + + TAB + + `${BULLET} **${alert.name}** [${alert.pluginid}] total: ${alert.instances.length}: ${NXT_LINE}`; + }); + msg = msg + NXT_LINE; } + } - msg = msg + NXT_LINE; - }); - if (msg.trim() !== "") { - msg = msg + NXT_LINE + runnerLink; - msg = msg + NXT_LINE + runnerID; + if (isFilteredSite(site)) { + if (site.ignoredAlerts.length !== 0) { + msg = `${msg} ${TAB} **Ignored Alerts** ${NXT_LINE}`; + site.ignoredAlerts.forEach((alert) => { + msg = + msg + + TAB + + `${BULLET} **${alert.name}** [${alert.pluginid}] total: ${alert.instances.length}: ${NXT_LINE}`; + }); + msg = msg + NXT_LINE; + } } - return msg; - }, - generateDifference: ( - newReport: Report | FilteredReport, - oldReport: Report | FilteredReport - ): Site[] => { - newReport.updated = false; - const siteClone: Site[] = []; - newReport.site.forEach((newReportSite) => { - // Check if the new report site already exists in the previous report - const previousSite = _.filter( - oldReport.site, - (s) => s["@name"] === newReportSite["@name"] + msg = msg + NXT_LINE; + }); + if (msg.trim() !== "") { + msg = msg + NXT_LINE + runnerLink; + msg = msg + NXT_LINE + runnerID; + } + return msg; +}; +const generateDifference = ( + newReport: Report | FilteredReport, + oldReport: Report | FilteredReport +): Site[] => { + newReport.updated = false; + const siteClone: Site[] = []; + newReport.site.forEach((newReportSite) => { + // Check if the new report site already exists in the previous report + const previousSite = _.filter( + oldReport.site, + (s) => s["@name"] === newReportSite["@name"] + ); + // If does not exists add it to the array without further processing + if (previousSite.length === 0) { + newReport.updated = true; + siteClone.push(newReportSite); + } else { + // deep clone the variable for further processing + const newSite = _.clone(newReportSite); + const currentAlerts = newReportSite.alerts; + const previousAlerts = previousSite[0].alerts; + + const newAlerts = _.differenceBy( + currentAlerts, + previousAlerts!, + "pluginid" + ); + let removedAlerts = _.differenceBy( + previousAlerts, + currentAlerts!, + "pluginid" ); - // If does not exists add it to the array without further processing - if (previousSite.length === 0) { - newReport.updated = true; - siteClone.push(newReportSite); - } else { - // deep clone the variable for further processing - const newSite = _.clone(newReportSite); - const currentAlerts = newReportSite.alerts; - const previousAlerts = previousSite[0].alerts; - const newAlerts = _.differenceBy( - currentAlerts, - previousAlerts!, + let ignoredAlerts: Alert[] = []; + if (isFilteredSite(newReportSite) && isFilteredSite(previousSite[0])) { + ignoredAlerts = _.differenceBy( + newReportSite["ignoredAlerts"], + previousSite[0]["ignoredAlerts"], "pluginid" ); - let removedAlerts = _.differenceBy( - previousAlerts, - currentAlerts!, - "pluginid" - ); - - let ignoredAlerts: Alert[] = []; - if (isFilteredSite(newReportSite) && isFilteredSite(previousSite[0])) { - ignoredAlerts = _.differenceBy( - newReportSite["ignoredAlerts"], - previousSite[0]["ignoredAlerts"], - "pluginid" - ); - } else if (isFilteredSite(newReportSite)) { - ignoredAlerts = newReportSite["ignoredAlerts"]; - } + } else if (isFilteredSite(newReportSite)) { + ignoredAlerts = newReportSite["ignoredAlerts"]; + } - removedAlerts = _.differenceBy( - removedAlerts, - ignoredAlerts, - "pluginid" - ); + removedAlerts = _.differenceBy(removedAlerts, ignoredAlerts, "pluginid"); - newSite.alerts = newAlerts; - (newSite as DifferenceSite).removedAlerts = removedAlerts; - (newSite as DifferenceSite).ignoredAlerts = ignoredAlerts; - siteClone.push(newSite); + newSite.alerts = newAlerts; + (newSite as DifferenceSite).removedAlerts = removedAlerts; + (newSite as DifferenceSite).ignoredAlerts = ignoredAlerts; + siteClone.push(newSite); - if ( - newAlerts.length !== 0 || - removedAlerts.length !== 0 || - ignoredAlerts.length !== 0 - ) { - newReport.updated = true; - } + if ( + newAlerts.length !== 0 || + removedAlerts.length !== 0 || + ignoredAlerts.length !== 0 + ) { + newReport.updated = true; } - }); - return siteClone; - }, - - readMDFile: async (reportName: string) => { - let res = ""; - try { - res = fs.readFileSync(reportName, { encoding: "base64" }); - } catch (err) { - console.log("error while reading the markdown file!"); } - return res; - }, + }); + return siteClone; +}; +const readMDFile = async (reportName: string) => { + let res = ""; + try { + res = fs.readFileSync(reportName, { encoding: "base64" }); + } catch (err) { + console.log("error while reading the markdown file!"); + } + return res; +}; +const checkIfAlertsExists = (jsonReport: Report) => { + return jsonReport.site.some((s) => { + return "alerts" in s && s.alerts!.length !== 0; + }); +}; +const filterReport = async ( + jsonReport: Report, + plugins: string[] +): Promise => { + jsonReport.site.forEach((s) => { + if ("alerts" in s && s.alerts!.length !== 0) { + console.log(`starting to filter the alerts for site: ${s["@name"]}`); + const newAlerts = s.alerts!.filter(function (e) { + return !plugins.includes(e.pluginid); + }); + const removedAlerts = s.alerts!.filter(function (e) { + return plugins.includes(e.pluginid); + }); + s.alerts = newAlerts; + (s as FilteredSite).ignoredAlerts = removedAlerts; - checkIfAlertsExists: (jsonReport: Report) => { - return jsonReport.site.some((s) => { - return "alerts" in s && s.alerts!.length !== 0; + console.log( + `#${newAlerts.length} alerts have been identified` + + ` and #${removedAlerts.length} alerts have been ignored for the site.` + ); + } + }); + return jsonReport as FilteredReport; +}; +const readPreviousReport = async ( + octokit: InstanceType["rest"], + owner: string, + repo: string, + workSpace: string, + runnerID: string, + artifactName = "zap_scan" +) => { + let previousReport; + try { + const artifactList = await octokit.actions.listWorkflowRunArtifacts({ + owner: owner, + repo: repo, + run_id: runnerID as unknown as number, }); - }, - filterReport: async ( - jsonReport: Report, - plugins: string[] - ): Promise => { - jsonReport.site.forEach((s) => { - if ("alerts" in s && s.alerts!.length !== 0) { - console.log(`starting to filter the alerts for site: ${s["@name"]}`); - const newAlerts = s.alerts!.filter(function (e) { - return !plugins.includes(e.pluginid); - }); - const removedAlerts = s.alerts!.filter(function (e) { - return plugins.includes(e.pluginid); - }); - s.alerts = newAlerts; - (s as FilteredSite).ignoredAlerts = removedAlerts; - - console.log( - `#${newAlerts.length} alerts have been identified` + - ` and #${removedAlerts.length} alerts have been ignored for the site.` - ); - } - }); - return jsonReport as FilteredReport; - }, + const artifacts = artifactList.data.artifacts; + let artifactID; + if (artifacts.length !== 0) { + artifacts.forEach((a) => { + if (a["name"] === artifactName) { + artifactID = a["id"]; + } + }); + } - readPreviousReport: async ( - octokit: InstanceType["rest"], - owner: string, - repo: string, - workSpace: string, - runnerID: string, - artifactName = "zap_scan" - ) => { - let previousReport; - try { - const artifactList = await octokit.actions.listWorkflowRunArtifacts({ + if (artifactID !== undefined) { + const download = await octokit.actions.downloadArtifact({ owner: owner, repo: repo, - run_id: runnerID as unknown as number, + artifact_id: artifactID, + archive_format: "zip", }); - const artifacts = artifactList.data.artifacts; - let artifactID; - if (artifacts.length !== 0) { - artifacts.forEach((a) => { - if (a["name"] === artifactName) { - artifactID = a["id"]; - } - }); - } - - if (artifactID !== undefined) { - const download = await octokit.actions.downloadArtifact({ - owner: owner, - repo: repo, - artifact_id: artifactID, - archive_format: "zip", - }); - - await new Promise((resolve) => - request(download.url) - .pipe(fs.createWriteStream(`${workSpace}/${artifactName}.zip`)) - .on("finish", () => { - resolve(); - }) - ); + await new Promise((resolve) => + request(download.url) + .pipe(fs.createWriteStream(`${workSpace}/${artifactName}.zip`)) + .on("finish", () => { + resolve(); + }) + ); - const zip = new AdmZip(`${workSpace}/${artifactName}.zip`); - const zipEntries = zip.getEntries(); + const zip = new AdmZip(`${workSpace}/${artifactName}.zip`); + const zipEntries = zip.getEntries(); - zipEntries.forEach(function (zipEntry) { - if (zipEntry.entryName === "report_json.json") { - previousReport = JSON.parse( - zipEntry.getData().toString("utf8") - ) as Report; - } - }); - } - } catch (e) { - console.log(`Error occurred while downloading the artifacts!`); + zipEntries.forEach(function (zipEntry) { + if (zipEntry.entryName === "report_json.json") { + previousReport = JSON.parse( + zipEntry.getData().toString("utf8") + ) as Report; + } + }); } - return previousReport; - }, + } catch (e) { + console.log(`Error occurred while downloading the artifacts!`); + } + return previousReport; +}; +const uploadArtifacts = async ( + rootDir: string, + mdReport: string, + jsonReport: string, + htmlReport: string, + artifactName = "zap_scan" +) => { + const artifactClient = create(); + const files = [ + `${rootDir}/${mdReport}`, + `${rootDir}/${jsonReport}`, + `${rootDir}/${htmlReport}`, + ]; + const rootDirectory = rootDir; + const options = { + continueOnError: true, + }; - uploadArtifacts: async ( - rootDir: string, - mdReport: string, - jsonReport: string, - htmlReport: string, - artifactName = "zap_scan" - ) => { - const artifactClient = create(); - const files = [ - `${rootDir}/${mdReport}`, - `${rootDir}/${jsonReport}`, - `${rootDir}/${htmlReport}`, - ]; - const rootDirectory = rootDir; - const options = { - continueOnError: true, - }; + await artifactClient.uploadArtifact( + artifactName, + files, + rootDirectory, + options + ); +}; - await artifactClient.uploadArtifact( - artifactName, - files, - rootDirectory, - options - ); - }, +const actionHelper = { + getRunnerID, + processLineByLine, + createMessage, + generateDifference, + readMDFile, + checkIfAlertsExists, + filterReport, + readPreviousReport, + uploadArtifacts, }; export default actionHelper; diff --git a/src/models/Alert.ts b/src/models/Alert.ts index d531381..8c9ce9c 100644 --- a/src/models/Alert.ts +++ b/src/models/Alert.ts @@ -3,5 +3,7 @@ import { Instance } from "./Instance"; export interface Alert { name: string; pluginid: string; + riskcode: "0" | "1" | "2" | "3"; + confidence: "0" | "1" | "2" | "3" | "4"; instances: Instance[]; } diff --git a/src/models/AlertFixture.ts b/src/models/AlertFixture.ts index db697b9..48b7faa 100644 --- a/src/models/AlertFixture.ts +++ b/src/models/AlertFixture.ts @@ -6,6 +6,9 @@ export class AlertFixture implements Alert { name = "Cross Site Scripting (Reflected)"; pluginid = "40012"; + confidence = "2" as const; + riskcode = "3" as const; + constructor(override: Partial = {}) { Object.assign(this, override); }