├── src ├── github │ ├── github-changes.ts │ ├── pull-request.ts │ ├── upload-artifacts.ts │ ├── github-context.ts │ ├── github-diffmap.ts │ ├── check.ts │ └── comment.ts ├── blackduck │ ├── detect │ │ ├── exit-codes.ts │ │ └── detect-manager.ts │ ├── blackduck-api.ts │ └── blackduck-utils.ts ├── diffmap.ts ├── polaris │ ├── changeset │ │ ├── IChangeSetCreator.ts │ │ ├── ChangeSetReplacement.ts │ │ ├── ChangeSetFileWriter.ts │ │ └── ChangeSetEnvironment.ts │ ├── model │ │ ├── PolarisInstall.ts │ │ ├── PolarisRunResult.ts │ │ ├── PolarisTaskInput.ts │ │ ├── PolarisConnection.ts │ │ ├── PolarisProxyInfo.ts │ │ └── PolarisAPI.ts │ ├── util │ │ ├── PolarisPlatformSupport.ts │ │ └── PolarisIssueWaiter.ts │ ├── cli │ │ ├── PolarisExecutableFinder.ts │ │ ├── PolarisRunner.ts │ │ └── PolarisInstaller.ts │ ├── input │ │ └── PolarisInputReader.ts │ └── service │ │ ├── PolarisJobService.ts │ │ ├── PolarisService.ts │ │ └── PolarisAPI.ts ├── paths.ts ├── SIGLogger.ts ├── gitlab │ ├── gitlab-utils.ts │ ├── gitlab-changes.ts │ ├── issues.ts │ ├── gitlab-diffmap.ts │ └── discussions.ts ├── _namespaces │ └── github.ts ├── security-gate.ts ├── models │ ├── sigma-schema.ts │ └── coverity-json-v7-schema.ts ├── sigma │ └── sigma-utils.ts ├── coverity │ ├── coverity-issue-mapper.ts │ ├── coverity-api.ts │ └── coverity-utils.ts ├── azure │ ├── review.ts │ └── azure-diffmap.ts └── index.ts ├── .prettierignore ├── publish.sh ├── .gitignore ├── .prettierrc.json ├── tsconfig.json ├── package.json └── LICENSE /src/github/github-changes.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm publish --access public 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /src/blackduck/detect/exit-codes.ts: -------------------------------------------------------------------------------- 1 | export const SUCCESS = 0 2 | export const POLICY_SEVERITY = 3 3 | -------------------------------------------------------------------------------- /src/diffmap.ts: -------------------------------------------------------------------------------- 1 | export type DiffMap = Map 2 | 3 | export interface Hunk { 4 | firstLine: number 5 | lastLine: number 6 | } -------------------------------------------------------------------------------- /src/polaris/changeset/IChangeSetCreator.ts: -------------------------------------------------------------------------------- 1 | var defaultPath = ".synopsys/polaris/changeSetFile.txt"; 2 | 3 | export default interface IChangeSetCreator { 4 | generate_change_set(cwd: string): Promise>; 5 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 999, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /src/paths.ts: -------------------------------------------------------------------------------- 1 | export function relatavize_path(prefix: string, path: string): string { 2 | let length = prefix.length 3 | if (!length) { 4 | length = 'undefined'.length 5 | } 6 | 7 | return path.substring(length + 1) 8 | } -------------------------------------------------------------------------------- /src/polaris/changeset/ChangeSetReplacement.ts: -------------------------------------------------------------------------------- 1 | export default class ChangeSetReplacement { 2 | replace_build_command(build_command: string, path: string): string { 3 | return build_command.split("$CHANGE_SET_FILE_PATH").join(path); 4 | } 5 | } -------------------------------------------------------------------------------- /src/polaris/model/PolarisInstall.ts: -------------------------------------------------------------------------------- 1 | export default class PolarisInstall { 2 | polaris_executable: string; 3 | polaris_home: string; 4 | constructor(polaris_executable: string, polaris_home: string) { 5 | this.polaris_executable = polaris_executable; 6 | this.polaris_home = polaris_home; 7 | } 8 | } -------------------------------------------------------------------------------- /src/polaris/model/PolarisRunResult.ts: -------------------------------------------------------------------------------- 1 | export default class PolarisRunResult { 2 | return_code: Number; 3 | scan_cli_json_path: string; 4 | constructor(return_code:Number, scan_cli_json_path:string) { 5 | this.return_code = return_code; 6 | this.scan_cli_json_path = scan_cli_json_path; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/polaris/model/PolarisTaskInput.ts: -------------------------------------------------------------------------------- 1 | import PolarisConnection from "./PolarisConnection"; 2 | 3 | export interface PolarisTaskInputs { 4 | polaris_connection: PolarisConnection 5 | build_command: string, 6 | should_wait_for_issues: boolean 7 | 8 | should_populate_changeset: boolean 9 | should_empty_changeset_fail: boolean 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2019", "es2016", "es2017", "dom"], 6 | "declaration": true, 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "types": ["node"], 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SIGLogger.ts: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | export const logger = winston.createLogger({ 3 | // level: 'debug', 4 | transports: [ 5 | new (winston.transports.Console)({ 6 | format: winston.format.combine( 7 | winston.format.colorize(), 8 | winston.format.simple() 9 | ) 10 | }) 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /src/polaris/model/PolarisConnection.ts: -------------------------------------------------------------------------------- 1 | import PolarisProxyInfo from "./PolarisProxyInfo"; 2 | 3 | export default class PolarisConnection { 4 | constructor(url: string, token: string, proxy: PolarisProxyInfo | undefined) { 5 | this.url = url; 6 | this.token = token; 7 | this.proxy = proxy; 8 | } 9 | url: string; 10 | token: string; 11 | proxy: PolarisProxyInfo | undefined; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/polaris/model/PolarisProxyInfo.ts: -------------------------------------------------------------------------------- 1 | export default class PolarisProxyInfo { 2 | proxy_url: string; 3 | proxy_username: string | undefined; 4 | proxy_password: string| undefined; 5 | constructor(proxy_url: string, proxy_username: string| undefined, proxy_password: string| undefined) { 6 | this.proxy_url = proxy_url; 7 | this.proxy_username = proxy_username; 8 | this.proxy_password = proxy_password; 9 | } 10 | } -------------------------------------------------------------------------------- /src/gitlab/gitlab-utils.ts: -------------------------------------------------------------------------------- 1 | import { Gitlab } from '@gitbeaker/node' 2 | import { ProjectSchema } from '@gitbeaker/core/dist/types/resources/Projects' 3 | import { DiscussionSchema } from '@gitbeaker/core/dist/types/templates/ResourceDiscussions' 4 | import { logger } from "../SIGLogger" 5 | 6 | export async function gitlabGetProject(gitlab_url: string, gitlab_token: string, project_id: string): Promise { 7 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 8 | 9 | logger.debug(`Getting project ${project_id}`) 10 | 11 | let project = await api.Projects.show(project_id) 12 | 13 | logger.debug(`Project name is ${project.name}`) 14 | 15 | return project 16 | } 17 | -------------------------------------------------------------------------------- /src/polaris/util/PolarisPlatformSupport.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | export default class PolarisPlatformSupport { 4 | platform_specific_cli_zip_url_fragment(client: string) { 5 | var platform = os.platform(); 6 | if (platform == "win32") { 7 | return "/api/tools/" + client + "_cli-win64.zip"; 8 | } else if (platform == "darwin") { 9 | return "/api/tools/" + client + "_cli-macosx.zip"; 10 | } else { 11 | return "/api/tools/" + client + "_cli-linux64.zip"; 12 | } 13 | } 14 | 15 | platform_specific_executable_name(client: string) { 16 | var platform = os.platform(); 17 | if (platform == "win32") { 18 | return client + ".exe"; 19 | } else { 20 | return client; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/github/pull-request.ts: -------------------------------------------------------------------------------- 1 | import {context, getOctokit} from "@actions/github"; 2 | import {githubGetPullRequestNumber} from "./github-context"; 3 | 4 | export async function githubGetPullRequestDiff(github_token: string): Promise { 5 | const octokit = getOctokit(github_token) 6 | 7 | const pullRequestNumber = githubGetPullRequestNumber() 8 | 9 | if (!pullRequestNumber) { 10 | return Promise.reject(Error('Could not get Pull Request Diff: Action was not running on a Pull Request')) 11 | } 12 | 13 | const response = await octokit.rest.pulls.get({ 14 | owner: context.repo.owner, 15 | repo: context.repo.repo, 16 | pull_number: pullRequestNumber, 17 | mediaType: { 18 | format: 'diff' 19 | } 20 | }) 21 | 22 | return response.data as unknown as string 23 | } -------------------------------------------------------------------------------- /src/_namespaces/github.ts: -------------------------------------------------------------------------------- 1 | import {RestEndpointMethodTypes} from '@octokit/rest' 2 | 3 | // @octokit/rest > Endpoints.d.ts > PullsGetResponseData 4 | export type PullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data'] 5 | 6 | // @octokit/rest > Endpoints.d.ts > /repos/{owner}/{repo}/pulls/{pull_number}/reviews > comments 7 | export type ReviewCommentsParameter = RestEndpointMethodTypes['pulls']['createReview']['parameters']['comments'] 8 | export type NewReviewComment = (ReviewCommentsParameter & Exclude)[number] 9 | 10 | // @octokit/rest > Endpoints.d.ts > /repos/{owner}/{repo}/pulls/{pull_number}/comments 11 | export type ExistingReviewComment = RestEndpointMethodTypes['pulls']['listReviewComments']['response']['data'][number] 12 | 13 | // @octokit/rest > Endpoints.d.ts > /repos/{owner}/{repo}/issues/{issue_number}/comments 14 | export type ExistingIssueComment = RestEndpointMethodTypes['issues']['listComments']['response']['data'][number] 15 | -------------------------------------------------------------------------------- /src/polaris/changeset/ChangeSetFileWriter.ts: -------------------------------------------------------------------------------- 1 | import {Logger} from "winston"; 2 | import {logger} from "../../SIGLogger"; 3 | 4 | var fs = require("fs"); 5 | const fse = require('fs-extra'); 6 | 7 | export default class ChangeSetFileWriter { 8 | log: Logger; 9 | constructor(log: Logger) { 10 | this.log = log; 11 | } 12 | async write_change_set_file(file: string, paths: Array) : Promise { //must return something 13 | await fse.ensureFile(file); 14 | 15 | return new Promise((resolve, reject) => { 16 | var content = paths.join("\n"); 17 | fs.writeFile(file, content, (err:any) => { 18 | if (err) { 19 | logger.error("Writing change set file failed: " + err) 20 | return reject(err); 21 | } else { 22 | logger.info("Created change set file: " + file); 23 | return resolve(0); 24 | } 25 | }); 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /src/github/upload-artifacts.ts: -------------------------------------------------------------------------------- 1 | import { warning, info } from '@actions/core' 2 | import { create, UploadOptions } from '@actions/artifact' 3 | import {logger} from "../SIGLogger"; 4 | 5 | export async function uploadArtifact(name: string, outputPath: string, files: string[]): Promise { 6 | const artifactClient = create() 7 | const options: UploadOptions = { 8 | continueOnError: false, 9 | retentionDays: 0 10 | } 11 | 12 | logger.info(`Attempting to upload ${name}...`) 13 | const uploadResponse = await artifactClient.uploadArtifact(name, files, outputPath, options) 14 | 15 | if (files.length === 0) { 16 | logger.warn(`Expected to upload ${name}, but the action couldn't find any. Was output-path set correctly?`) 17 | } else if (uploadResponse.failedItems.length > 0) { 18 | logger.warn(`An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.`) 19 | } else { 20 | logger.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/github/github-context.ts: -------------------------------------------------------------------------------- 1 | import { context } from '@actions/github' 2 | import { PullRequest } from '../_namespaces/Github' 3 | 4 | const prEvents = ['pull_request', 'pull_request_review', 'pull_request_review_comment'] 5 | 6 | export function githubIsPullRequest(): boolean { 7 | return prEvents.includes(context.eventName) 8 | } 9 | 10 | export function githubGetSha(): string { 11 | let sha = context.sha 12 | if (githubIsPullRequest()) { 13 | const pull = context.payload.pull_request as PullRequest 14 | if (pull?.head.sha) { 15 | sha = pull?.head.sha 16 | } 17 | } 18 | 19 | return sha 20 | } 21 | 22 | export function githubGetPullRequestNumber(): number | undefined { 23 | let pr_number = undefined 24 | if (githubIsPullRequest()) { 25 | const pull = context.payload.pull_request as PullRequest 26 | if (pull?.number) { 27 | pr_number = pull.number 28 | } 29 | } 30 | 31 | return pr_number 32 | } 33 | 34 | export function githubRelativizePath(path: string): string { 35 | let length = process.env.GITHUB_WORKSPACE?.length 36 | if (!length) { 37 | length = 'undefined'.length 38 | } 39 | 40 | return path.substring(length + 1) 41 | } 42 | -------------------------------------------------------------------------------- /src/polaris/changeset/ChangeSetEnvironment.ts: -------------------------------------------------------------------------------- 1 | import {Logger} from "winston"; 2 | const path = require('path'); 3 | 4 | 5 | export default class ChangeSetEnvironment { 6 | log: Logger; 7 | env: any; 8 | constructor(log:Logger, env:any) { 9 | this.log = log; 10 | this.env = env; 11 | } 12 | 13 | set_enable_incremental() { 14 | this.env["POLARIS_FF_ENABLE_COVERITY_INCREMENTAL"] = "true"; 15 | } 16 | 17 | is_file_path_present(): boolean { 18 | if ("CHANGE_SET_FILE_PATH" in this.env) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | 25 | set_default_file_path(cwd: string) { 26 | this.env["CHANGE_SET_FILE_PATH"] = path.join(cwd, ".synopsys", "polaris", "changeSetFile.txt"); 27 | } 28 | 29 | get_file_path(): string { 30 | return this.env["CHANGE_SET_FILE_PATH"] 31 | } 32 | 33 | get_or_create_file_path(cwd: string): string { 34 | if (this.is_file_path_present()) { 35 | return this.get_file_path(); 36 | } else { 37 | this.set_default_file_path(cwd); 38 | return this.get_file_path(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "synopsys-sig-node", 3 | "version": "1.1.70-beta.2", 4 | "description": "Node.js Library for Integrating with Synopsys AST Solutions", 5 | "scripts": { 6 | "clean": "rm -rf lib", 7 | "build": "tsc", 8 | "format": "prettier --write '**/*.ts'", 9 | "format-check": "prettier --check '**/*.ts'", 10 | "lint": "eslint src/**/*.ts", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "all": "npm run build && npm run format" 13 | }, 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.js", 16 | "files": [ 17 | "lib" 18 | ], 19 | "author": "Synopsys Community", 20 | "license": "Apache-2.0", 21 | "devDependencies": { 22 | "@types/node": "^17.0.21", 23 | "prettier": "^2.5.1" 24 | }, 25 | "dependencies": { 26 | "@actions/artifact": "^1.0.0", 27 | "@actions/core": "^1.6.0", 28 | "@actions/exec": "^1.1.1", 29 | "@actions/github": "^5.0.0", 30 | "@actions/tool-cache": "^1.7.2", 31 | "@gitbeaker/node": "^35.4.0", 32 | "@octokit/rest": "^18.12.0", 33 | "adm-zip": "^0.5.9", 34 | "await-exec": "^0.1.2", 35 | "axios": "^0.26.1", 36 | "azure-devops-node-api": "^11.1.1", 37 | "case-insensitive-map": "^1.0.1", 38 | "java-caller": "^2.4.0", 39 | "jsonpath": "^1.1.1", 40 | "moment": "^2.29.1", 41 | "proxy-agent": "^5.0.0", 42 | "winston": "^3.6.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/polaris/cli/PolarisExecutableFinder.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | import PolarisPlatformSupport from "../util/PolarisPlatformSupport"; 4 | 5 | export default class PolarisExecutableFinder { 6 | log: any; 7 | platformSupport: PolarisPlatformSupport; 8 | constructor(log:any, platformSupport: PolarisPlatformSupport) { 9 | this.log = log; 10 | this.platformSupport = platformSupport; 11 | } 12 | 13 | async find_executable(polaris_install: string): Promise { 14 | var polarisInternalFolders = fs.readdirSync(polaris_install); 15 | var polarisFolder = path.join(polaris_install, polarisInternalFolders[0]); 16 | var bin = path.join(polarisFolder, "bin"); 17 | var exes = fs.readdirSync(bin); 18 | for (var i in exes) { 19 | var file = exes[i]; 20 | await this.ensure_executable(path.join(bin, file)); 21 | } 22 | var polaris_exe = this.platformSupport.platform_specific_executable_name("polaris"); 23 | return path.join(bin, polaris_exe); 24 | } 25 | 26 | private async ensure_executable(exe: string): Promise { 27 | if (fs.existsSync(exe)) { 28 | this.log.debug(`Ensuring ${exe} is executable.`) 29 | fs.chmodSync(exe, 0o775); 30 | return exe; 31 | } else { 32 | throw new Error(`Could not make ${exe} executable.`); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/gitlab/gitlab-changes.ts: -------------------------------------------------------------------------------- 1 | import {Gitlab} from "@gitbeaker/node"; 2 | import {logger} from "../SIGLogger"; 3 | 4 | export async function gitlabGetChangesForMR(gitlab_url: string, gitlab_token: string, project_id: string, merge_request_iid: number): Promise> { 5 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 6 | 7 | logger.debug(`Getting merge request #${merge_request_iid} in project #${project_id}`) 8 | logger.debug(`GITLAB_TOKEN=${gitlab_token} GITLAB_URL=${gitlab_url}`) 9 | let merge_request = await api.MergeRequests.show(project_id, merge_request_iid) 10 | logger.debug(`Merge Request title is ${merge_request.title}`) 11 | 12 | let changed_files: string[] = [] 13 | let changes = await api.MergeRequests.changes(project_id, merge_request_iid) 14 | if (changes && changes.changes) { 15 | for (const change of changes.changes) { 16 | logger.debug(`Change detected to ${change.new_path}`) 17 | const filename = change.new_path 18 | changed_files.push(filename) 19 | } 20 | 21 | } 22 | 23 | return(changed_files) 24 | } 25 | /* 26 | 27 | changes = mr.changes() 28 | if debug: print(f"DEBUG: mr.changes()={json.dumps(changes, indent=4)}") 29 | 30 | file_changes = dict() 31 | with open(output, "w") as fp: 32 | for change in changes['changes']: 33 | if debug: print(f"DEBUG: change={change}") 34 | filename = change['new_path'] 35 | if debug: print(f"DEBUG: filename={filename}") 36 | fp.write(f"{filename}\n") 37 | 38 | 39 | 40 | */ -------------------------------------------------------------------------------- /src/security-gate.ts: -------------------------------------------------------------------------------- 1 | import {logger} from "./SIGLogger"; 2 | 3 | const CaseInsensitiveMap = require('case-insensitive-map') 4 | 5 | export function readSecurityGateFiltersFromString(securityGateString: string): typeof CaseInsensitiveMap { 6 | const securityGateJson = JSON.parse(securityGateString) 7 | let securityGateMap = new CaseInsensitiveMap() 8 | logger.debug(`Reading security gate filters`) 9 | 10 | Object.keys(securityGateJson).forEach(function(key) { 11 | var values = securityGateJson[key] 12 | logger.debug(` ${key}`) 13 | securityGateMap.set(key, new Array()) 14 | for (const value of values) { 15 | securityGateMap.get(key)?.push(value) 16 | logger.debug(` ${value}`) 17 | } 18 | }) 19 | 20 | return(securityGateMap) 21 | } 22 | 23 | export function isIssueAllowed(securityFilters: typeof CaseInsensitiveMap, 24 | severity: string, 25 | cwe: string, 26 | isNew: boolean = false): boolean { 27 | if (securityFilters.get("status") && isNew && securityFilters.get("status")?.indexOf("new") >= 0) { 28 | return(false) 29 | } 30 | 31 | if (securityFilters.get("severity") && securityFilters.get("severity")?.indexOf(severity) >= 0) { 32 | return(false) 33 | } 34 | 35 | if (securityFilters.get("cwe")) { 36 | const cweValues = cwe.split(', ') 37 | for (const cweValue of cweValues) { 38 | for (cwe of securityFilters.get("cwe")) { 39 | if (cwe == cweValue) { 40 | return (false) 41 | } 42 | } 43 | } 44 | } 45 | 46 | return(true) 47 | } -------------------------------------------------------------------------------- /src/polaris/input/PolarisInputReader.ts: -------------------------------------------------------------------------------- 1 | import { PolarisTaskInputs } from "../model/PolarisTaskInput"; 2 | import PolarisProxyInfo from "../model/PolarisProxyInfo"; 3 | import proxy from "proxy-agent"; 4 | import PolarisConnection from "../model/PolarisConnection"; 5 | 6 | export default class PolarisInputReader { 7 | getPolarisInputs(polaris_url: string, polaris_token: string, 8 | proxy_url: string, proxy_username: string, proxy_password: string, 9 | build_command: string, 10 | should_wait_for_issues: boolean, 11 | should_changeset: boolean, 12 | should_changeset_fail: boolean): PolarisTaskInputs { 13 | var polaris_proxy_info: PolarisProxyInfo | undefined = undefined; 14 | 15 | if (proxy_url && proxy_url.length > 0 && proxy_username && proxy_username.length > 0 && 16 | proxy_password && proxy_password.length > 0) { 17 | polaris_proxy_info = new PolarisProxyInfo(proxy_url, proxy_username, proxy_password); 18 | } else { 19 | polaris_proxy_info = undefined 20 | } 21 | 22 | if (polaris_url.endsWith("/") || polaris_url.endsWith("\\")) { 23 | polaris_url = polaris_url.slice(0, -1); 24 | } 25 | 26 | if (build_command.includes("--incremental")) { 27 | should_changeset = true 28 | } 29 | 30 | return { 31 | polaris_connection: new PolarisConnection(polaris_url, polaris_token, polaris_proxy_info), 32 | build_command: build_command, 33 | should_wait_for_issues: should_wait_for_issues, 34 | should_empty_changeset_fail: should_changeset_fail, 35 | should_populate_changeset: should_changeset 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/github/github-diffmap.ts: -------------------------------------------------------------------------------- 1 | import {DiffMap} from "../diffmap"; 2 | 3 | const UNKNOWN_FILE = "Unknown" 4 | 5 | export function githubGetDiffMap(rawDiff: string): DiffMap { 6 | console.info('Gathering diffs...') 7 | const diffMap = new Map() 8 | 9 | let path = UNKNOWN_FILE 10 | for (const line of rawDiff.split('\n')) { 11 | if (line.startsWith('diff --git')) { 12 | // TODO: Handle spaces in path 13 | // TODO: Will this continue to work with other GitHub integrations? 14 | // path = `${process.env.GITHUB_WORKSPACE}/${line.split(' ')[2].substring(2)}` 15 | path = `${line.split(' ')[2].substring(2)}` 16 | 17 | if (path === undefined) { 18 | path = UNKNOWN_FILE 19 | } 20 | 21 | diffMap.set(path, []) 22 | } 23 | 24 | if (line.startsWith('@@')) { 25 | let changedLines = line.substring(3) 26 | changedLines = changedLines.substring(0, changedLines.indexOf(' @@')) 27 | 28 | const linesAddedPosition = changedLines.indexOf('+') 29 | if (linesAddedPosition > -1) { 30 | // We only care about the right side because Coverity can only analyze what's there, not what used to be --rotte FEB 2022 31 | const linesAddedString = changedLines.substring(linesAddedPosition + 1) 32 | const separatorPosition = linesAddedString.indexOf(',') 33 | 34 | const startLine = parseInt(linesAddedString.substring(0, separatorPosition)) 35 | const lineCount = parseInt(linesAddedString.substring(separatorPosition + 1)) 36 | const endLine = startLine + lineCount - 1 37 | 38 | if (!diffMap.has(path)) { 39 | diffMap.set(path, []) 40 | } 41 | console.info(`Added ${path}: ${startLine} to ${endLine}`) 42 | diffMap.get(path)?.push({firstLine: startLine, lastLine: endLine}) 43 | } 44 | } 45 | } 46 | 47 | return diffMap 48 | } -------------------------------------------------------------------------------- /src/gitlab/issues.ts: -------------------------------------------------------------------------------- 1 | import {logger} from "../SIGLogger"; 2 | import {IssueSchema} from "@gitbeaker/core/dist/types/resources/Issues"; 3 | import {Gitlab} from "@gitbeaker/node"; 4 | 5 | export async function gitlabGetIssues(gitlab_url: string, gitlab_token: string, project_id: string, 6 | title_search: string): Promise> { 7 | const api = new Gitlab({host: gitlab_url, token: gitlab_token}) 8 | // GitBeaker returns a relatively awkward data structure, so we will return a more convenient one 9 | let return_issues = Array() 10 | 11 | let issues = await api.Issues.all({ 12 | projectId: project_id, 13 | search: title_search, 14 | sate: "opened" 15 | }) 16 | 17 | for (const issue of issues) { 18 | // TODO: The title search does not seem to work above, implement here 19 | let title = issue.title as string 20 | if (title.includes(title_search)) { 21 | logger.debug(`GitLab Issue with title: ${issue.title} description: ${issue.description}`) 22 | return_issues.push(issue) 23 | } 24 | } 25 | 26 | return return_issues 27 | } 28 | 29 | export async function gitlabCreateIssue(gitlab_url: string, gitlab_token: string, project_id: string, title: string, 30 | description: string): Promise { 31 | const api = new Gitlab({host: gitlab_url, token: gitlab_token}) 32 | 33 | let new_issue = await api.Issues.create(project_id, { 34 | title: title, 35 | description: description, 36 | issue_type: "issue" 37 | }) 38 | 39 | return new_issue.iid 40 | } 41 | 42 | export async function gitlabCloseIssue(gitlab_url: string, gitlab_token: string, project_id: string, 43 | issue_id: number): Promise { 44 | const api = new Gitlab({host: gitlab_url, token: gitlab_token}) 45 | 46 | await api.Issues.edit(project_id, issue_id, { 47 | state_event: "close" 48 | }) 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /src/models/sigma-schema.ts: -------------------------------------------------------------------------------- 1 | export interface SigmaIssuesView { 2 | revision: string 3 | issues: SigmaIssueWrapper 4 | } 5 | 6 | // Issues 7 | 8 | export interface SigmaIssueWrapper { 9 | created: string 10 | issues: SigmaIssueOccurrence[] 11 | } 12 | 13 | export interface SigmaIssueOccurrence { 14 | checker_id: string 15 | uuid: string 16 | summary: string 17 | desc: string 18 | remediation: string 19 | severity: SigmaIssueSeverity 20 | taxonomies: SigmaIssueTaxonomy 21 | filepath: string 22 | function: string 23 | language: string 24 | location: SigmaIssueLocations 25 | issue_type: string 26 | tags: Array 27 | fixes?: SigmaIssueFix[] 28 | stateOnServer?: StateOnServer 29 | } 30 | 31 | export interface SigmaIssueSeverity { 32 | level: string 33 | impact: string 34 | likelihood: string 35 | } 36 | 37 | export interface SigmaIssueTaxonomy { 38 | cwe: Array 39 | } 40 | 41 | export interface SigmaIssueLocations { 42 | start: SigmaIssueLocation 43 | end: SigmaIssueLocation 44 | } 45 | 46 | export interface SigmaIssueLocation { 47 | line: number 48 | column: number 49 | byte: number 50 | } 51 | 52 | export interface SigmaIssueFix { 53 | desc: string 54 | actions: SigmaIssueFixAction[] 55 | } 56 | 57 | export interface SigmaIssueFixAction { 58 | location: SigmaIssueLocations 59 | kind: string 60 | contents: string 61 | } 62 | 63 | export interface StateOnServer { 64 | cid: number 65 | presentInReferenceSnapshot: boolean 66 | firstDetectedDateTime: string 67 | stream: string 68 | components: string[] 69 | componentOwners?: any 70 | cached: boolean 71 | retrievalDateTime: string 72 | ownerLdapServerName: string 73 | triage: Triage 74 | customTriage: CustomTriage 75 | } 76 | 77 | export interface Triage { 78 | classification: string 79 | action: string 80 | fixTarget: string 81 | severity: string 82 | legacy: string 83 | owner: string 84 | externalReference: string 85 | } 86 | 87 | export interface CustomTriage { 88 | // set of key-value pairs 89 | } 90 | -------------------------------------------------------------------------------- /src/gitlab/gitlab-diffmap.ts: -------------------------------------------------------------------------------- 1 | import {Gitlab} from "@gitbeaker/node"; 2 | import {logger} from "../SIGLogger"; 3 | 4 | export async function gitlabGetDiffMap(gitlab_url: string, gitlab_token: string, project_id: string, merge_request_iid: number): Promise> { 5 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 6 | 7 | logger.debug(`Getting commits for merge request #${merge_request_iid} in project #${project_id}`) 8 | let commits = await api.MergeRequests.commits(project_id, merge_request_iid) 9 | 10 | const diffMap = new Map() 11 | 12 | for (const commit of commits) { 13 | logger.debug(`Commit #${commit.id}: ${commit.title}`) 14 | let diffs = await api.Commits.diff(project_id, commit.id) 15 | for (const diff of diffs) { 16 | logger.debug(` Diff file: ${diff.new_path} diff: ${diff.diff}`) 17 | 18 | const path = diff.new_path 19 | diffMap.set(path, []) 20 | 21 | const diff_text = diff.diff 22 | if (diff_text.startsWith('@@')) { 23 | let changedLines = diff_text.substring(3) 24 | changedLines = changedLines.substring(0, changedLines.indexOf(' @@')) 25 | 26 | const linesAddedPosition = changedLines.indexOf('+') 27 | if (linesAddedPosition > -1) { 28 | // We only care about the right side because SI can only analyze what's there, not what used to be 29 | const linesAddedString = changedLines.substring(linesAddedPosition + 1) 30 | const separatorPosition = linesAddedString.indexOf(',') 31 | 32 | const startLine = parseInt(linesAddedString.substring(0, separatorPosition)) 33 | const lineCount = parseInt(linesAddedString.substring(separatorPosition + 1)) 34 | const endLine = startLine + lineCount - 1 35 | 36 | if (!diffMap.has(path)) { 37 | diffMap.set(path, []) 38 | } 39 | logger.debug(`Added ${path}: ${startLine} to ${endLine}`) 40 | diffMap.get(path)?.push({firstLine: startLine, lastLine: endLine, sha: commit.id}) 41 | } 42 | } 43 | } 44 | } 45 | 46 | return diffMap 47 | } -------------------------------------------------------------------------------- /src/polaris/util/PolarisIssueWaiter.ts: -------------------------------------------------------------------------------- 1 | import PolarisService from "../service/PolarisService"; 2 | 3 | const fs = require('fs'); 4 | const json_path = require('jsonpath'); 5 | import PolarisJobService from '../service/PolarisJobService'; 6 | import {logger} from "../../SIGLogger"; 7 | 8 | export default class PolarisIssueWaiter { 9 | log: any; 10 | constructor(log:any) { 11 | this.log = log; 12 | } 13 | 14 | async wait_for_issues(scan_cli_json_path: String, polaris_service: PolarisService): Promise { 15 | var scan_json_text = fs.readFileSync(scan_cli_json_path); 16 | var scan_json = JSON.parse(scan_json_text); 17 | 18 | var issue_counts = json_path.query(scan_json, "$.issueSummary.total"); 19 | 20 | var job_status_urls = json_path.query(scan_json, "$.tools[*].jobStatusUrl"); 21 | if (job_status_urls.length > 0) { 22 | this.log.info("Waiting for jobs: " + job_status_urls.length) 23 | var polaris_job_service = new PolarisJobService(this.log, polaris_service); 24 | await polaris_job_service.waitForJobsToEnd(job_status_urls); 25 | } else { 26 | this.log.info("No jobs were found to wait for.") 27 | } 28 | 29 | var project_id = json_path.query(scan_json, "$.projectInfo.projectId") 30 | var branch_id = json_path.query(scan_json, "$.projectInfo.branchId") 31 | var revision_id = json_path.query(scan_json, "$.projectInfo.revisionId") 32 | 33 | var issue_api_url = json_path.query(scan_json, "$.scanInfo.issueApiUrl"); 34 | if (issue_api_url.length > 0) { 35 | this.log.info("Getting issues from Polaris Software Integrity Platform server.") 36 | var issue_response = await polaris_service.fetch_issue_data(issue_api_url[0]); 37 | issue_counts = json_path.query(issue_response.data, "$.data..attributes.value"); 38 | } 39 | 40 | if (issue_counts.length != 0) { 41 | var total_count = issue_counts.reduce((a:any, b:any) => a + b, 0) 42 | this.log.info("Total issues found : " + total_count + "(This will may be filtered for reporting") 43 | return total_count; 44 | } else { 45 | this.log.info("Did not find any issue counts.") 46 | return null; 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/github/check.ts: -------------------------------------------------------------------------------- 1 | import { debug, info, warning } from '@actions/core' 2 | import { context, getOctokit } from '@actions/github' 3 | import {logger} from "../SIGLogger" 4 | import {githubGetSha} from "./github-context"; 5 | 6 | export async function githubCreateCheck(checkName: string, githubToken: string): Promise { 7 | const octokit = getOctokit(githubToken) 8 | 9 | const head_sha = githubGetSha() 10 | 11 | logger.info(`Creating ${checkName}...`) 12 | const response = await octokit.rest.checks.create({ 13 | owner: context.repo.owner, 14 | repo: context.repo.repo, 15 | name: checkName, 16 | head_sha 17 | }) 18 | 19 | if (response.status !== 201) { 20 | logger.warn(`Unexpected status code recieved when creating ${checkName}: ${response.status}`) 21 | logger.debug(JSON.stringify(response, null, 2)) 22 | } else { 23 | logger.info(`${checkName} created`) 24 | } 25 | 26 | return new GitHubCheck(checkName, response.data.id, githubToken) 27 | } 28 | 29 | export class GitHubCheck { 30 | checkName: string 31 | checkRunId: number 32 | githubToken: string 33 | 34 | constructor(checkName: string, checkRunId: number, githubToken: string) { 35 | this.checkName = checkName 36 | this.checkRunId = checkRunId 37 | this.githubToken = githubToken 38 | } 39 | 40 | async passCheck(summary: string, text: string) { 41 | return this.finishCheck('success', summary, text) 42 | } 43 | 44 | async failCheck(summary: string, text: string) { 45 | return this.finishCheck('failure', summary, text) 46 | } 47 | 48 | async skipCheck() { 49 | return this.finishCheck('skipped', `${this.checkName} was skipped`, '') 50 | } 51 | 52 | async cancelCheck() { 53 | return this.finishCheck('cancelled', `${this.checkName} Check could not be completed`, `Something went wrong and the ${this.checkName} could not be completed. Check your action logs for more details.`) 54 | } 55 | 56 | private async finishCheck(conclusion: string, summary: string, text: string) { 57 | const octokit = getOctokit(this.githubToken) 58 | 59 | const response = await octokit.rest.checks.update({ 60 | owner: context.repo.owner, 61 | repo: context.repo.repo, 62 | check_run_id: this.checkRunId, 63 | status: 'completed', 64 | conclusion, 65 | output: { 66 | title: this.checkName, 67 | summary, 68 | text 69 | } 70 | }) 71 | 72 | if (response.status !== 200) { 73 | warning(`Unexpected status code recieved when creating check: ${response.status}`) 74 | debug(JSON.stringify(response, null, 2)) 75 | } else { 76 | info(`${this.checkName} updated`) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/polaris/service/PolarisJobService.ts: -------------------------------------------------------------------------------- 1 | const json_path = require('jsonpath'); 2 | 3 | import PolarisService from "./PolarisService" 4 | 5 | export default class PolarisJobService { 6 | log: any; 7 | polaris_client: PolarisService 8 | constructor(log:any, polaris_client: PolarisService) { 9 | this.log = log; 10 | this.polaris_client = polaris_client; 11 | } 12 | 13 | async waitForJobsToEnd(status_job_urls: string[]) { 14 | var self = this; 15 | await asyncForEach(status_job_urls, async function(job:string) { 16 | await self.waitForJobToEnd(job); 17 | await self.checkJobSuccess(job); 18 | }); 19 | } 20 | 21 | async waitForJobToEnd(status_job_url: string) { 22 | var running = true; 23 | while (running) { 24 | var jobEnded = await this.hasJobEnded(status_job_url); 25 | 26 | if (jobEnded) { 27 | running = false; 28 | } else { 29 | this.log.info("Waiting 2 seconds for job to complete."); 30 | await sleep(2000); 31 | } 32 | } 33 | } 34 | 35 | async hasJobEnded(status_job_url: string): Promise { 36 | var job_response = await this.polaris_client.get_job(status_job_url); 37 | var status = json_path.query(job_response.data, "$.data.attributes.status.state"); 38 | if (containsAny(status, ["QUEUED", "RUNNING", "DISPATCHED"])) { 39 | return false; 40 | } 41 | return true; 42 | } 43 | 44 | async checkJobSuccess(status_job_url: string) { 45 | var job_response = await this.polaris_client.get_job(status_job_url); 46 | var status = json_path.query(job_response.data, "$.data.attributes.status.state"); 47 | if (containsAny(status, ["FAILED"])) { 48 | var reason = json_path.query(job_response.data, "$.data.attributes.failureInfo.userFiendlyFailureReason"); 49 | if (reason.length > 0) { 50 | this.log.error("Check the job status in Polaris Software Integrity Platform for more details.") 51 | throw new Error(JSON.stringify(reason)); 52 | } 53 | } 54 | return true; 55 | } 56 | } 57 | 58 | async function asyncForEach(array: any[], func: any) { 59 | for (let index = 0; index < array.length; index++) { 60 | await func(array[index], index, array); 61 | } 62 | } 63 | 64 | function containsAny(array: any[], elements: any[]) { 65 | return array.some(r=> elements.indexOf(r) >= 0); 66 | } 67 | 68 | async function sleep(ms:any) { 69 | return new Promise((resolve) => { 70 | setTimeout(resolve, ms); 71 | }); 72 | } -------------------------------------------------------------------------------- /src/blackduck/detect/detect-manager.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {logger} from "../../SIGLogger"; 3 | import {exec} from "@actions/exec" 4 | import {cacheFile, downloadTool, find} from "@actions/tool-cache"; 5 | 6 | const DETECT_BINARY_REPO_URL = 'https://sig-repo.synopsys.com' 7 | export const TOOL_NAME = 'detect' 8 | 9 | const DETECT_LATEST_VERSION="7.11.1" 10 | 11 | export async function githubFindOrDownloadDetect(detect_version: string=DETECT_LATEST_VERSION): Promise { 12 | const jarName = `synopsys-detect-${detect_version}.jar` 13 | 14 | const cachedDetect = find(TOOL_NAME, detect_version) 15 | if (cachedDetect) { 16 | return path.resolve(cachedDetect, jarName) 17 | } 18 | 19 | const detectDownloadUrl = createDetectDownloadUrl() 20 | 21 | return ( 22 | downloadTool(detectDownloadUrl) 23 | .then(detectDownloadPath => cacheFile(detectDownloadPath, jarName, TOOL_NAME, detect_version)) 24 | //TODO: Jarsigner? 25 | .then(cachedFolder => path.resolve(cachedFolder, jarName)) 26 | ) 27 | } 28 | 29 | export async function findOrDownloadDetect(download_dir: string, verbose: boolean=false, detect_version: string=DETECT_LATEST_VERSION): Promise { 30 | const jarName = `synopsys-detect-${detect_version}.jar` 31 | 32 | const detectDownloadUrl = createDetectDownloadUrl() 33 | 34 | const Downloader = require("nodejs-file-downloader") 35 | 36 | logger.info(`Downloading ${detectDownloadUrl}`) 37 | const downloader = new Downloader({ 38 | url: detectDownloadUrl, 39 | directory: download_dir, 40 | onProgress: function (percentage: any, chunk: any, remainingSize: any) { 41 | if (verbose) { 42 | logger.info(`%${percentage} ${remainingSize} bytes remaining`) 43 | } 44 | }, 45 | }); 46 | 47 | try { 48 | await downloader.download(); 49 | } catch (error) { 50 | logger.error(`Unable to download file: ${error}`) 51 | } 52 | 53 | return(path.resolve(download_dir, jarName)) 54 | } 55 | 56 | export async function githubRunDetect(detectPath: string, detectArguments: string[]): Promise { 57 | return exec(`java`, ['-jar', detectPath].concat(detectArguments), { ignoreReturnCode: true }) 58 | } 59 | 60 | export async function runDetect(detectPath: string, detectArguments: string[]): Promise { 61 | // NOTE: Uses GitHub Actions interface to exec 62 | return exec(`java`, ['-jar', detectPath].concat(detectArguments), { ignoreReturnCode: true }) 63 | } 64 | 65 | export function createDetectDownloadUrl(repoUrl = DETECT_BINARY_REPO_URL, detect_version: string = DETECT_LATEST_VERSION): string { 66 | return `${repoUrl}/bds-integrations-release/com/synopsys/integration/synopsys-detect/${detect_version}/synopsys-detect-${detect_version}.jar` 67 | } 68 | -------------------------------------------------------------------------------- /src/polaris/cli/PolarisRunner.ts: -------------------------------------------------------------------------------- 1 | import PolarisRunResult from "../model/PolarisRunResult"; 2 | const fs = require('fs'); 3 | const fse = require('fs-extra'); 4 | const path = require('path'); 5 | const urlParser = require('url'); 6 | import PolarisInstall from "../model/PolarisInstall"; 7 | import PolarisConnection from "../model/PolarisConnection"; 8 | import {logger} from "../../SIGLogger"; 9 | import {exec} from "@actions/exec"; 10 | 11 | export default class PolarisRunner { 12 | log: any; 13 | constructor(log:any) { 14 | this.log = log; 15 | } 16 | 17 | async execute_cli(connection: PolarisConnection, polaris_install: PolarisInstall, cwd: string, build_command: string):Promise { 18 | var env: any = process.env; 19 | 20 | env["POLARIS_SERVER_URL"] = connection.url; 21 | env["POLARIS_ACCESS_TOKEN"] = connection.token; 22 | 23 | if (connection.proxy != undefined) { 24 | var proxyOpts = urlParser.parse(connection.proxy.proxy_url); 25 | if (connection.proxy.proxy_username && connection.proxy.proxy_password) { 26 | proxyOpts.auth = connection.proxy.proxy_username + ":" + connection.proxy.proxy_password; 27 | } 28 | env["HTTPS_PROXY"] = urlParser.format(proxyOpts); 29 | } 30 | 31 | if ("POLARIS_HOME" in env) { 32 | this.log.info("A POLARIS_HOME exists, will not attempt to override.") 33 | } else { 34 | var override_home = polaris_install.polaris_home; 35 | if (!fs.existsSync(override_home)) { 36 | this.log.info("Creating plugin Polaris home: " + override_home) 37 | fse.ensureDirSync(override_home); 38 | } else { 39 | this.log.debug("Polaris home already exists, it will not be created.") 40 | } 41 | 42 | if (fs.existsSync(override_home)) { 43 | this.log.info("Set POLARIS_HOME to directory: " + override_home) 44 | env["POLARIS_HOME"] = override_home 45 | } else { 46 | this.log.error("Unable to create a POLARIS_HOME and env variable was not set. Will not override. Try creating POLARIS_HOME on the agent or ensuring agent has access.") 47 | } 48 | } 49 | 50 | logger.info(`Executing ${polaris_install.polaris_executable} with line=${build_command}`) 51 | 52 | var return_code = await exec(polaris_install.polaris_executable, 53 | build_command.split(' '), 54 | { ignoreReturnCode: true }) 55 | 56 | var synopsysFolder = path.join(cwd, ".synopsys"); 57 | var polarisFolder = path.join(synopsysFolder, "polaris"); 58 | var scanJsonFile = path.join(polarisFolder, "cli-scan.json"); 59 | 60 | delete process.env["HTTPS_PROXY"]; 61 | 62 | return new PolarisRunResult(return_code, scanJsonFile); 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/sigma/sigma-utils.ts: -------------------------------------------------------------------------------- 1 | import {SigmaIssueOccurrence} from "../models/sigma-schema" 2 | import {DiffMap} from "../diffmap" 3 | import {logger} from "../SIGLogger" 4 | import * as fs from "fs" 5 | 6 | export const SIGMA_COMMENT_PREFACE = '' 7 | 8 | export function sigmaIsInDiff(issue: SigmaIssueOccurrence, diffMap: DiffMap): boolean { 9 | //logger.debug(`Look for ${issue.filepath} in diffMap`) 10 | const diffHunks = diffMap.get(issue.filepath) 11 | 12 | //logger.debug(`diffHunks=${diffHunks}`) 13 | 14 | if (!diffHunks) { 15 | return false 16 | } 17 | 18 | for (const hunk of diffHunks) { 19 | if (issue.location.start.line >= hunk.firstLine && issue.location.start.line <= hunk.lastLine) { 20 | //logger.debug(`found location in diffHunk`) 21 | return true 22 | } 23 | } 24 | 25 | return false 26 | 27 | // JC: For some reason the filter statement below is not working for sigma. 28 | // TODO: Come back to this. 29 | //return diffHunks.filter(hunk => hunk.firstLine <= issue.location.start.line).some(hunk => issue.location.start.line <= hunk.lastLine) 30 | } 31 | 32 | function get_line(filename: string, line_no: number): string { 33 | var data = fs.readFileSync(filename, 'utf8') 34 | var lines = data.split('\n') 35 | 36 | if (+line_no > lines.length) { 37 | throw new Error('File end reached without finding line') 38 | } 39 | 40 | return lines[+line_no] 41 | } 42 | 43 | export const sigmaUuidCommentOf = (issue: SigmaIssueOccurrence): string => `` 44 | 45 | export function sigmaCreateMessageFromIssue(issue: SigmaIssueOccurrence): string { 46 | const issueName = issue.summary 47 | const checkerNameString = issue.checker_id 48 | const impactString = issue.severity ? issue.severity.impact : 'Unknown' 49 | const cweString = issue.taxonomies?.cwe ? `, CWE-${issue.taxonomies?.cwe[0]}` : '' 50 | const description = issue.desc 51 | const remediation = issue.remediation ? issue.remediation : 'Not available' 52 | const remediationString = issue.remediation ? `## How to fix\r\n ${remediation}` : '' 53 | let suggestion = undefined 54 | 55 | // JC: Assume only one fix for now 56 | // TODO: Follow up with roadmap plans for fixes 57 | if (issue.fixes) { 58 | let fix = issue.fixes[0] 59 | 60 | let path = issue.filepath 61 | 62 | // TODO: try/catch for get_line in case file doesn't exist 63 | let current_line = get_line(path, fix.actions[0].location.start.line - 1) 64 | logger.debug(`current_line=${current_line}`) 65 | 66 | suggestion = current_line.substring(0, fix.actions[0].location.start.column - 1) + fix.actions[0].contents + current_line.substring(fix.actions[0].location.end.column - 1, current_line.length) 67 | 68 | logger.debug(`suggestion=${suggestion}`) 69 | } 70 | 71 | const suggestionString = suggestion ? '\n```suggestion\n' + suggestion + '\n```' : '' 72 | logger.debug(`suggestionString=${suggestionString}`) 73 | 74 | return `${SIGMA_COMMENT_PREFACE} 75 | ${sigmaUuidCommentOf(issue)} 76 | # :warning: Sigma Issue - ${issueName} 77 | ${description} 78 | 79 | _${impactString} Impact${cweString}_ ${checkerNameString} 80 | 81 | ${remediationString} 82 | 83 | ${suggestionString} 84 | ` 85 | } 86 | -------------------------------------------------------------------------------- /src/github/comment.ts: -------------------------------------------------------------------------------- 1 | import {ExistingIssueComment, ExistingReviewComment, NewReviewComment} from "../_namespaces/github"; 2 | import {context, getOctokit} from "@actions/github"; 3 | import {githubGetPullRequestNumber} from "./github-context"; 4 | import {logger} from "../SIGLogger"; 5 | 6 | export async function githubGetExistingReviewComments(github_token: string): Promise { 7 | const octokit = getOctokit(github_token) 8 | 9 | const pullRequestNumber = githubGetPullRequestNumber() 10 | if (!pullRequestNumber) { 11 | return Promise.reject(Error('Could not create Pull Request Review Comment: Action was not running on a Pull Request')) 12 | } 13 | 14 | const reviewCommentsResponse = await octokit.rest.pulls.listReviewComments({ 15 | owner: context.repo.owner, 16 | repo: context.repo.repo, 17 | pull_number: pullRequestNumber 18 | }) 19 | 20 | return reviewCommentsResponse.data 21 | } 22 | 23 | export async function githubUpdateExistingReviewComment(github_token: string, commentId: number, body: string): Promise { 24 | const octokit = getOctokit(github_token) 25 | 26 | octokit.rest.pulls.updateReviewComment({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | comment_id: commentId, 30 | body 31 | }) 32 | } 33 | 34 | export async function githubCreateReview(github_token: string, comments: NewReviewComment[], event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT' = 'COMMENT'): Promise { 35 | const octokit = getOctokit(github_token) 36 | 37 | const pullRequestNumber = githubGetPullRequestNumber() 38 | if (!pullRequestNumber) { 39 | return Promise.reject(Error('Could not create Pull Request Review Comment: Action was not running on a Pull Request')) 40 | } 41 | 42 | logger.debug(`PR number: ${pullRequestNumber} owner: ${context.repo.owner} repo: ${context.repo.repo} event: ${event}`) 43 | octokit.rest.pulls.createReview({ 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | pull_number: pullRequestNumber, 47 | event, 48 | comments 49 | }) 50 | } 51 | 52 | export async function githubGetExistingIssueComments(github_token: string): Promise { 53 | const octokit = getOctokit(github_token) 54 | 55 | const {data: existingComments} = await octokit.rest.issues.listComments({ 56 | issue_number: context.issue.number, 57 | owner: context.repo.owner, 58 | repo: context.repo.repo 59 | }) 60 | 61 | return existingComments 62 | } 63 | 64 | export async function githubUpdateExistingIssueComment(github_token: string, commentId: number, body: string): Promise { 65 | const octokit = getOctokit(github_token) 66 | 67 | octokit.rest.issues.updateComment({ 68 | issue_number: context.issue.number, 69 | owner: context.repo.owner, 70 | repo: context.repo.repo, 71 | comment_id: commentId, 72 | body 73 | }) 74 | } 75 | 76 | export async function githubCreateIssueComment(github_token: string, body: string): Promise { 77 | const octokit = getOctokit(github_token) 78 | 79 | octokit.rest.issues.createComment({ 80 | issue_number: context.issue.number, 81 | owner: context.repo.owner, 82 | repo: context.repo.repo, 83 | body 84 | }) 85 | } -------------------------------------------------------------------------------- /src/coverity/coverity-issue-mapper.ts: -------------------------------------------------------------------------------- 1 | import {CoverityApiService, 2 | ICoverityResponseCell, 3 | KEY_ACTION, KEY_CID, KEY_CLASSIFICATION, KEY_FIRST_SNAPSHOT_ID, KEY_LAST_SNAPSHOT_ID, KEY_MERGE_KEY 4 | } from './coverity-api' 5 | import {logger} from "../SIGLogger"; 6 | 7 | const PAGE_SIZE = 500 8 | 9 | export class CoverityProjectIssue { 10 | cid: string 11 | mergeKey: string | null 12 | action: string 13 | classification: string 14 | firstSnapshotId: string 15 | lastSnapshotId: string 16 | 17 | constructor(cid: string, mergeKey: string | null, action: string, classification: string, firstSnapshotId: string, lastSnapshotId: string) { 18 | this.cid = cid 19 | this.mergeKey = mergeKey 20 | this.action = action 21 | this.classification = classification 22 | this.firstSnapshotId = firstSnapshotId 23 | this.lastSnapshotId = lastSnapshotId 24 | } 25 | } 26 | 27 | // FIXME This is very inefficient for projects with lots of issues. When filtering by mergeKey is fixed, we should use that instead. 28 | export async function coverityMapMatchingMergeKeys(coverity_url: string, 29 | coverity_username: string, 30 | coverity_passphrase: string, 31 | coverity_project_name: string, 32 | relevantMergeKeys: Set): Promise> { 33 | logger.info('Checking Coverity server for existing issues...') 34 | const apiService = new CoverityApiService(coverity_url, coverity_username, coverity_passphrase) 35 | 36 | let totalRows = 0 37 | let offset = 0 38 | 39 | const mergeKeyToProjectIssue = new Map() 40 | 41 | while (offset <= totalRows && mergeKeyToProjectIssue.size < relevantMergeKeys.size) { 42 | try { 43 | const covProjectIssues = await apiService.findIssues(coverity_project_name, offset, PAGE_SIZE) 44 | totalRows = covProjectIssues.totalRows 45 | logger.debug(`Found ${covProjectIssues?.rows.length} potentially matching issues on the server`) 46 | 47 | covProjectIssues.rows 48 | .map(row => toProjectIssue(row)) 49 | .filter(projectIssue => projectIssue.mergeKey != null) 50 | .filter(projectIssue => relevantMergeKeys.has(projectIssue.mergeKey as string)) 51 | .forEach(projectIssue => mergeKeyToProjectIssue.set(projectIssue.mergeKey as string, projectIssue)) 52 | } catch (error: any) { 53 | return Promise.reject(error) 54 | } 55 | offset += PAGE_SIZE 56 | } 57 | 58 | logger.info(`Found ${mergeKeyToProjectIssue.size} existing issues`) 59 | return mergeKeyToProjectIssue 60 | } 61 | 62 | function toProjectIssue(issueRows: ICoverityResponseCell[]): CoverityProjectIssue { 63 | let cid = '' 64 | let mergeKey = null 65 | let action = '' 66 | let classification = '' 67 | let firstSnapshotId = '' 68 | let lastSnapshotId = '' 69 | for (const issueCol of issueRows) { 70 | if (issueCol.key == KEY_CID) { 71 | cid = issueCol.value 72 | } else if (issueCol.key == KEY_MERGE_KEY) { 73 | mergeKey = issueCol.value 74 | } else if (issueCol.key == KEY_ACTION) { 75 | action = issueCol.value 76 | } else if (issueCol.key == KEY_CLASSIFICATION) { 77 | classification = issueCol.value 78 | } else if (issueCol.key == KEY_FIRST_SNAPSHOT_ID) { 79 | firstSnapshotId = issueCol.value 80 | } else if (issueCol.key == KEY_LAST_SNAPSHOT_ID) { 81 | lastSnapshotId = issueCol.value 82 | } 83 | } 84 | return new CoverityProjectIssue(cid, mergeKey, action, classification, firstSnapshotId, lastSnapshotId) 85 | } 86 | -------------------------------------------------------------------------------- /src/azure/review.ts: -------------------------------------------------------------------------------- 1 | import {IGitApi} from "azure-devops-node-api/GitApi"; 2 | import { 3 | Comment, CommentPosition, 4 | CommentThreadContext, 5 | GitPullRequestCommentThread 6 | } from "azure-devops-node-api/interfaces/GitInterfaces"; 7 | import {SigmaIssueOccurrence} from "../models/sigma-schema"; 8 | 9 | export const UNKNOWN_FILE = 'Unknown File' 10 | 11 | export async function azGetExistingReviewThreads(git_agent: IGitApi, repo_id: string, pull_id: number): Promise { 12 | let threads: GitPullRequestCommentThread[] = [] 13 | 14 | threads = await git_agent.getThreads(repo_id, pull_id) 15 | if (threads && threads.length > 0) { 16 | for (const thread of threads) { 17 | //logger.info(`DEBUG: thread id=${thread.id}`) 18 | if (thread.comments) { 19 | for (const comment of thread.comments) { 20 | //logger.info(`DEBUG: comment=${comment.content}`) 21 | } 22 | } 23 | } 24 | } 25 | 26 | return threads 27 | } 28 | 29 | export async function azUpdateComment(git_agent: IGitApi, repo_id: string, pull_id: number, thread_id: number, 30 | comment_id: number, comment_body: string): Promise { 31 | let updated_comment: Comment = {} 32 | updated_comment.content = comment_body 33 | 34 | let comment = await git_agent.updateComment(updated_comment, repo_id, pull_id, thread_id, comment_id) 35 | 36 | return true 37 | } 38 | export async function azCreateReviewComment(git_agent: IGitApi, repo_id: string, pull_id: number, 39 | path: string, line: number, 40 | comment_body: string): Promise { 41 | let comment: Comment = {} 42 | comment.content = comment_body 43 | comment.parentCommentId = 0 44 | comment.commentType = 1 45 | 46 | let thread: GitPullRequestCommentThread = {} 47 | thread.threadContext = {} 48 | 49 | if (path && line) { 50 | thread.threadContext.filePath = path 51 | thread.threadContext.rightFileStart = {} 52 | thread.threadContext.rightFileStart.line = line 53 | thread.threadContext.rightFileStart.offset = 1 54 | thread.threadContext.rightFileEnd = {} 55 | thread.threadContext.rightFileEnd.line = line 56 | thread.threadContext.rightFileEnd.offset = 1 57 | } 58 | thread.status = 1 // Active 59 | 60 | thread.comments = [ comment ] 61 | 62 | let new_thread = await git_agent.createThread(thread, repo_id, pull_id) 63 | 64 | return true 65 | } 66 | 67 | export async function azCreateSigmaReviewComment(git_agent: IGitApi, repo_id: string, pull_id: number, 68 | issue: SigmaIssueOccurrence, comment_body: string): Promise { 69 | let comment: Comment = {} 70 | comment.content = comment_body 71 | comment.parentCommentId = 0 72 | comment.commentType = 1 73 | 74 | let thread: GitPullRequestCommentThread = {} 75 | thread.threadContext = {} 76 | thread.threadContext.filePath = "/" + issue.filepath 77 | thread.threadContext.rightFileStart = {} 78 | thread.threadContext.rightFileStart.line = issue.location.start.line 79 | thread.threadContext.rightFileStart.offset = 1 80 | thread.threadContext.rightFileEnd = {} 81 | thread.threadContext.rightFileEnd.line = issue.location.start.line 82 | thread.threadContext.rightFileEnd.offset = 1 83 | thread.status = 1 // Active 84 | 85 | thread.comments = [ comment ] 86 | 87 | let new_thread = await git_agent.createThread(thread, repo_id, pull_id) 88 | 89 | return true 90 | } -------------------------------------------------------------------------------- /src/coverity/coverity-api.ts: -------------------------------------------------------------------------------- 1 | import {debug} from '@actions/core' 2 | import {IRequestQueryParams} from 'typed-rest-client/Interfaces' 3 | import {BasicCredentialHandler} from 'typed-rest-client/Handlers' 4 | import {RestClient} from 'typed-rest-client/RestClient' 5 | 6 | export const KEY_CID = 'cid' 7 | export const KEY_MERGE_KEY = 'mergeKey' 8 | export const KEY_ACTION = 'action' 9 | export const KEY_CLASSIFICATION = 'classification' 10 | export const KEY_FIRST_SNAPSHOT_ID = 'firstSnapshotId' 11 | export const KEY_LAST_SNAPSHOT_ID = 'lastDetectedId' 12 | 13 | export interface ICoverityIssuesSearchResponse { 14 | offset: number 15 | totalRows: number 16 | columns: string[] 17 | rows: ICoverityResponseCell[][] 18 | } 19 | 20 | export interface ICoverityResponseCell { 21 | key: string 22 | value: string 23 | } 24 | 25 | interface ICoverityIssueOccurrenceRequest { 26 | filters: ICoverityRequestFilter[] 27 | snapshotScope?: ICoveritySnapshotScopeFilter 28 | columns: string[] 29 | } 30 | 31 | interface ICoverityRequestFilter { 32 | columnKey: string 33 | matchMode: string 34 | matchers: ICoverityRequestFilterMatcher[] 35 | } 36 | 37 | interface ICoverityRequestFilterMatcher { 38 | type: string 39 | id?: string 40 | class?: string 41 | key?: string 42 | name?: string 43 | date?: string 44 | } 45 | 46 | interface ICoveritySnapshotScopeFilter { 47 | show?: ICoveritySnapshotScope 48 | compareTo?: ICoveritySnapshotScope 49 | } 50 | 51 | interface ICoveritySnapshotScope { 52 | scope: string 53 | includeOutdatedSnapshots: boolean 54 | } 55 | 56 | export class CoverityApiService { 57 | coverityUrl: string 58 | restClient: RestClient 59 | 60 | constructor(coverityUrl: string, coverityUsername: string, coverityPassword: string, 61 | client_name: string="Generic Coverity REST API Client") { 62 | this.coverityUrl = cleanUrl(coverityUrl) 63 | 64 | const authHandler = new BasicCredentialHandler(coverityUsername, coverityPassword, true) 65 | this.restClient = new RestClient(client_name, this.coverityUrl, [authHandler], { 66 | headers: { 67 | Accept: 'application/json', 68 | 'Content-Type': 'application/json' 69 | } 70 | }) 71 | } 72 | 73 | async findIssues(projectName: string, offset: number, limit: number): Promise { 74 | const requestBody: ICoverityIssueOccurrenceRequest = { 75 | filters: [ 76 | { 77 | columnKey: 'project', 78 | matchMode: 'oneOrMoreMatch', 79 | matchers: [ 80 | { 81 | class: 'Project', 82 | name: projectName, 83 | type: 'nameMatcher' 84 | } 85 | ] 86 | } 87 | ], 88 | columns: [KEY_CID, KEY_MERGE_KEY, KEY_ACTION, KEY_CLASSIFICATION, KEY_FIRST_SNAPSHOT_ID, KEY_LAST_SNAPSHOT_ID] 89 | } 90 | const queryParameters: IRequestQueryParams = { 91 | params: { 92 | locale: 'en_us', 93 | offset, 94 | rowCount: limit, 95 | includeColumnLabels: 'true', 96 | queryType: 'bySnapshot', 97 | sortOrder: 'asc' 98 | } 99 | } 100 | const response = await this.restClient.create('/api/v2/issues/search', requestBody, {queryParameters}) 101 | if (response.statusCode < 200 || response.statusCode >= 300) { 102 | debug(`Coverity response error: ${response.result}`) 103 | return Promise.reject(`Failed to retrieve issues from Coverity for project '${projectName}': ${response.statusCode}`) 104 | } 105 | return Promise.resolve(response.result as ICoverityIssuesSearchResponse) 106 | } 107 | } 108 | 109 | export function cleanUrl(url: string): string { 110 | if (url && url.endsWith('/')) { 111 | return url.slice(0, url.length - 1) 112 | } 113 | return url 114 | } 115 | -------------------------------------------------------------------------------- /src/azure/azure-diffmap.ts: -------------------------------------------------------------------------------- 1 | import {IGitApi} from "azure-devops-node-api/GitApi"; 2 | import {FileDiffParams, FileDiffsCriteria} from "azure-devops-node-api/interfaces/GitInterfaces"; 3 | import {logger} from "../SIGLogger"; 4 | 5 | export async function azGetDiffMap(git_agent: IGitApi, repo_id: string, project_id: string, pull_id: number): Promise> { 6 | const diffMap = new Map() 7 | 8 | //logger.info(`DEBUG: getDiffMap for repo: ${repo_id} project: ${project_id} pull: ${pull_id}`) 9 | 10 | let commits = await git_agent.getPullRequestCommits(repo_id, pull_id) 11 | if (commits) { 12 | for (const commit of commits) { 13 | if (commit.commitId) { 14 | let changes = await git_agent.getChanges(commit.commitId, repo_id) 15 | if (changes && changes.changes) { 16 | for (const change of changes.changes) { 17 | if (change && change.item) { 18 | logger.debug(`Azure change id=${change.changeId} item path=${change.item.path} commitid=${change.item.commitId} url=${change.item.url} content=${change.item.content} type=${change.changeType}`) 19 | 20 | let diff_criteria: FileDiffsCriteria = {} 21 | diff_criteria.baseVersionCommit = change.item.commitId 22 | diff_criteria.targetVersionCommit = change.item.commitId 23 | 24 | let fileDiffParam = {} 25 | fileDiffParam.path = change.item.path 26 | diff_criteria.fileDiffParams = [ fileDiffParam ] 27 | logger.debug(`Azure fileDiffParam len=${diff_criteria.fileDiffParams.length} dd=${diff_criteria.fileDiffParams}`) 28 | 29 | let diffs = undefined 30 | try { 31 | diffs = await git_agent.getFileDiffs(diff_criteria, project_id, repo_id) 32 | } catch (error) { 33 | logger.info(`Unable to get diffs for ${change.item.path}: ${error}, skipping`) 34 | continue 35 | } 36 | for (const diff of diffs) { 37 | logger.debug(`Azure diff path=${diff.path}`) 38 | 39 | if (diff.lineDiffBlocks) { 40 | for (const diffBlock of diff.lineDiffBlocks) { 41 | logger.debug(`Azure diff block mlineStart=${diffBlock.modifiedLineNumberStart} mlineCount=${diffBlock.modifiedLinesCount}`) 42 | logger.debug(`Azure diff block olineStart=${diffBlock.originalLineNumberStart} olineCount=${diffBlock.originalLinesCount}`) 43 | 44 | if (change && change.item && change.item.path) { 45 | if (!diffMap.has(change.item.path.substring(1))) { 46 | diffMap.set(change.item.path.substring(1), []) 47 | } 48 | if (diffBlock.modifiedLineNumberStart && diffBlock.modifiedLinesCount) { 49 | logger.debug(`Added ${change.item.path.substring(1)}: ${diffBlock.modifiedLineNumberStart} to ${diffBlock.modifiedLineNumberStart + diffBlock.modifiedLinesCount}`) 50 | 51 | diffMap.get(change.item.path.substring(1))?.push( 52 | { 53 | firstLine: diffBlock.modifiedLineNumberStart, 54 | lastLine: diffBlock.modifiedLineNumberStart + diffBlock.modifiedLinesCount 55 | }) 56 | } 57 | 58 | } 59 | } 60 | 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | } 69 | } 70 | 71 | return diffMap 72 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Azure 2 | export { 3 | azGetDiffMap 4 | } from './azure/azure-diffmap' 5 | export { 6 | azGetExistingReviewThreads, 7 | azUpdateComment, 8 | azCreateReviewComment, 9 | } from './azure/review' 10 | // GitHub 11 | export { 12 | githubIsPullRequest, 13 | githubGetSha, 14 | githubGetPullRequestNumber, 15 | githubRelativizePath 16 | } from './github/github-context' 17 | export { 18 | githubGetDiffMap 19 | } from './github/github-diffmap' 20 | export { 21 | githubGetPullRequestDiff 22 | } from './github/pull-request' 23 | export { 24 | githubCreateCheck, 25 | GitHubCheck 26 | } from './github/check' 27 | export { 28 | uploadArtifact 29 | } from './github/upload-artifacts' 30 | export { 31 | githubGetExistingReviewComments, 32 | githubUpdateExistingReviewComment, 33 | githubCreateReview, 34 | githubGetExistingIssueComments, 35 | githubUpdateExistingIssueComment, 36 | githubCreateIssueComment, 37 | } from './github/comment' 38 | // GitLab 39 | export { 40 | gitlabGetDiffMap 41 | } from './gitlab/gitlab-diffmap' 42 | export { 43 | gitlabGetDiscussions, 44 | gitlabUpdateNote, 45 | gitlabCreateDiscussion, 46 | gitlabCreateDiscussionWithoutPosition 47 | } from './gitlab/discussions' 48 | export { 49 | gitlabGetIssues, 50 | gitlabCreateIssue, 51 | gitlabCloseIssue 52 | } from './gitlab/issues' 53 | export { 54 | gitlabGetProject, 55 | } from './gitlab/gitlab-utils' 56 | // Black Duck 57 | export { 58 | IBlackduckView, 59 | IUpgradeGuidance, 60 | IRecommendedVersion, 61 | IComponentSearchResult, 62 | IComponentVersion, 63 | IComponentVulnerability, 64 | ICvssView, 65 | IRapidScanResults, 66 | IRapidScanVulnerability, 67 | IRapidScanLicense, 68 | BlackduckApiService 69 | } from './blackduck/blackduck-api' 70 | export { 71 | githubFindOrDownloadDetect, 72 | findOrDownloadDetect, 73 | githubRunDetect, 74 | runDetect 75 | } from './blackduck/detect/detect-manager' 76 | export { 77 | createRapidScanReportString, 78 | createRapidScanReport, 79 | IComponentReport, 80 | createComponentReport, 81 | createComponentLicenseReports, 82 | createComponentVulnerabilityReports, 83 | ILicenseReport, 84 | createLicenseReport, 85 | IVulnerabilityReport, 86 | createVulnerabilityReport, 87 | IUpgradeReport, 88 | createUpgradeReport 89 | } from './blackduck/blackduck-utils' 90 | // Coverity 91 | export { 92 | CoverityIssuesView, 93 | CoverityIssueOccurrence, 94 | CoverityEvent, 95 | CoverityCheckerProperties, 96 | CoverityStateOnServer, 97 | CoverityTriage, 98 | CoverityCustomTriage, 99 | CoverityError, 100 | CoverityDesktopAnalysisSettings, 101 | CoverityReferenceSnapshotDetails, 102 | CoverityPortableAnalysisSettings, 103 | CoverityFileCheckerOption 104 | } from './models/coverity-json-v7-schema' 105 | export { 106 | ICoverityIssuesSearchResponse, 107 | ICoverityResponseCell, 108 | CoverityApiService, 109 | cleanUrl 110 | } from './coverity/coverity-api' 111 | export { 112 | CoverityProjectIssue, 113 | coverityMapMatchingMergeKeys 114 | } from './coverity/coverity-issue-mapper' 115 | export { 116 | coverityIsPresent, 117 | coverityCreateNoLongerPresentMessage, 118 | coverityCreateReviewCommentMessage, 119 | coverityCreateIssueCommentMessage, 120 | coverityIsInDiff, 121 | coverityCreateIssue, 122 | COVERITY_COMMENT_PREFACE, 123 | COVERITY_NOT_PRESENT, 124 | COVERITY_PRESENT, 125 | COVERITY_UNKNOWN_FILE 126 | } from './coverity/coverity-utils' 127 | // Sigma 128 | export { 129 | SigmaIssuesView, 130 | SigmaIssueWrapper, 131 | SigmaIssueOccurrence, 132 | SigmaIssueSeverity, 133 | SigmaIssueTaxonomy, 134 | SigmaIssueLocations, 135 | SigmaIssueLocation, 136 | SigmaIssueFix, 137 | SigmaIssueFixAction, 138 | StateOnServer, 139 | Triage, 140 | CustomTriage 141 | } from './models/sigma-schema' 142 | export { 143 | SIGMA_COMMENT_PREFACE, 144 | sigmaIsInDiff, 145 | sigmaUuidCommentOf, 146 | sigmaCreateMessageFromIssue 147 | } from './sigma/sigma-utils' 148 | // Polaris 149 | // ... 150 | // Other 151 | export { logger } from './SIGLogger' 152 | export { 153 | DiffMap, 154 | Hunk 155 | } from './diffmap' 156 | export { 157 | relatavize_path 158 | } from './paths' 159 | export { 160 | readSecurityGateFiltersFromString, 161 | isIssueAllowed 162 | } from './security-gate' 163 | 164 | -------------------------------------------------------------------------------- /src/models/coverity-json-v7-schema.ts: -------------------------------------------------------------------------------- 1 | export interface CoverityIssuesView { 2 | type: string 3 | formatVersion: number 4 | suppressedIssueCount: number 5 | issues: CoverityIssueOccurrence[] 6 | error?: CoverityError 7 | warnings: CoverityError[] 8 | desktopAnalysisSettings: CoverityDesktopAnalysisSettings 9 | } 10 | 11 | // Issues 12 | 13 | export interface CoverityIssueOccurrence { 14 | mergeKey: string 15 | occurrenceCountForMK: number 16 | occurrenceNumberInMK: number 17 | referenceOccurrenceCountForMK: number 18 | checkerName: string 19 | subcategory: string 20 | type: string 21 | subtype: string 22 | extra: string 23 | domain: string 24 | language?: string 25 | 'code-language'?: string 26 | mainEventFilePathname: string 27 | strippedMainEventFilePathname: string 28 | mainEventLineNumber: number 29 | properties: Map | any 30 | functionDisplayName?: string 31 | functionMangledName?: string 32 | localStatus?: string 33 | ordered: boolean 34 | events: CoverityEvent[] 35 | checkerProperties?: CoverityCheckerProperties 36 | stateOnServer?: CoverityStateOnServer 37 | } 38 | 39 | export interface CoverityEvent { 40 | covLStrEventDescription: string 41 | eventDescription: string 42 | eventNumber: number 43 | eventTreePosition: string 44 | eventSet: number 45 | eventTag: string 46 | filePathname: string 47 | strippedFilePathname: string 48 | lineNumber: number 49 | main: boolean 50 | moreInformationId?: string 51 | remediation: boolean 52 | events?: CoverityEvent[] 53 | } 54 | 55 | export interface CoverityCheckerProperties { 56 | category: string 57 | categoryDescription: string 58 | cweCategory: string 59 | issueKinds: string[] 60 | eventSetCaptions: string[] 61 | impact: string 62 | impactDescription: string 63 | subcategoryLocalEffect: string 64 | subcategoryLongDescription: string 65 | subcategoryShortDescription: string 66 | MISRACategory?: string 67 | } 68 | 69 | export interface CoverityStateOnServer { 70 | cid: number 71 | presentInReferenceSnapshot: boolean 72 | firstDetectedDateTime: string 73 | stream: string 74 | components: string[] 75 | componentOwners?: any 76 | cached: boolean 77 | retrievalDateTime: string 78 | ownerLdapServerName: string 79 | triage: CoverityTriage 80 | customTriage: CoverityCustomTriage 81 | } 82 | 83 | export interface CoverityTriage { 84 | classification: string 85 | action: string 86 | fixTarget: string 87 | severity: string 88 | legacy: string 89 | owner: string 90 | externalReference: string 91 | } 92 | 93 | export interface CoverityCustomTriage { 94 | // set of key-value pairs 95 | } 96 | 97 | // Error/Warnings 98 | 99 | export interface CoverityError { 100 | errorType: string 101 | errorSubType: string 102 | errorMessage: any 103 | // ... other errorType-specific attributes ... 104 | } 105 | 106 | // Desktop Analysis Settings 107 | 108 | export interface CoverityDesktopAnalysisSettings { 109 | analysisDateTime: string 110 | covRunDesktopArgs: string[] 111 | effectiveStripPaths: string[] 112 | analysisScopePathnames: string[] 113 | strippedAnalysisScopePathnames: string[] 114 | auxiliaryScopePathnames: string[] 115 | strippedAuxiliaryScopePathnames: string[] 116 | relativeTo?: string 117 | intermediateDir: string 118 | effectiveAnalysisSettings: CoverityPortableAnalysisSettings 119 | referenceSnapshot?: CoverityReferenceSnapshotDetails 120 | } 121 | 122 | export interface CoverityReferenceSnapshotDetails { 123 | snapshotId: number 124 | codeVersionDateTime: string 125 | description: string 126 | version: string 127 | analysisVersion: string 128 | analysisVersionOverride: string 129 | target: string 130 | analysisSettings: CoverityPortableAnalysisSettings 131 | } 132 | 133 | export interface CoverityPortableAnalysisSettings { 134 | covAnalyzeArgs: string[] 135 | fbExcludeConfigurations: string[] 136 | fbIncludeConfiguration: string 137 | fileCheckerOptions: CoverityFileCheckerOption[] 138 | } 139 | 140 | export interface CoverityFileCheckerOption { 141 | checkerName: string 142 | optionName: string 143 | fileContents: string 144 | } 145 | -------------------------------------------------------------------------------- /src/polaris/cli/PolarisInstaller.ts: -------------------------------------------------------------------------------- 1 | import PolarisExecutableFinder from "./PolarisExecutableFinder"; 2 | import PolarisPlatformSupport from "../util/PolarisPlatformSupport"; 3 | import PolarisService from "../service/PolarisService"; 4 | import PolarisInstall from "../model/PolarisInstall"; 5 | 6 | const moment = require("moment"); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const fse = require('fs-extra'); 10 | const zipper = require('adm-zip'); 11 | 12 | export default class PolarisInstaller { 13 | log: any; 14 | executable_finder: PolarisExecutableFinder; 15 | platform_support: PolarisPlatformSupport; 16 | polaris_service: PolarisService; 17 | 18 | static default_installer(log:any, polaris_service: PolarisService) { 19 | const platform_support = new PolarisPlatformSupport(); 20 | const executable_finder = new PolarisExecutableFinder(log, platform_support); 21 | return new PolarisInstaller(log, executable_finder, platform_support, polaris_service); 22 | } 23 | 24 | constructor(log:any, executable_finder: PolarisExecutableFinder, platform_support: PolarisPlatformSupport, polaris_service: PolarisService) { 25 | this.log = log; 26 | this.executable_finder = executable_finder; 27 | this.platform_support = platform_support; 28 | this.polaris_service = polaris_service; 29 | } 30 | 31 | async install_or_locate_polaris(polaris_url: string, polaris_install_path: string) : Promise { 32 | var polaris_cli_name = "polaris"; // used to be "swip" 33 | 34 | var polaris_cli_location = path.resolve(polaris_install_path, "polaris"); 35 | var version_file = path.join(polaris_cli_location, "version.txt"); 36 | var relative_cli_url = this.platform_support.platform_specific_cli_zip_url_fragment(polaris_cli_name); 37 | var cli_url = polaris_url + relative_cli_url; 38 | var synopsys_path = path.resolve(polaris_install_path, ".synopsys"); 39 | var polaris_home = path.resolve(synopsys_path, "polaris"); 40 | 41 | this.log.info(`Using polaris cli location: ` + polaris_cli_location) 42 | this.log.info(`Using polaris cli url: ` + cli_url) 43 | this.log.debug("Checking for version file: " + version_file) 44 | 45 | var download_cli = false; 46 | var available_version_date = await this.polaris_service.fetch_cli_modified_date(cli_url); 47 | if (fs.existsSync(version_file)) { 48 | this.log.debug("Version file exists.") 49 | var current_version_date = moment(fs.readFileSync(version_file, { encoding: 'utf8' })); 50 | this.log.debug("Current version: " + current_version_date.format()) 51 | this.log.debug("Available version: " + available_version_date.format()) 52 | if (current_version_date.isBefore(available_version_date)) { 53 | this.log.info("Downloading Polaris CLI because a newer version is available.") 54 | download_cli = true; 55 | } else { 56 | this.log.info("Existing Polaris CLI will be used.") 57 | } 58 | } else { 59 | this.log.info("Downloading Polaris CLI because a version file did not exist.") 60 | download_cli = true; 61 | } 62 | 63 | if (download_cli) { 64 | if (fs.existsSync(polaris_cli_location)) { 65 | this.log.info(`Cleaning up the Polaris installation directory: ${polaris_cli_location}`); 66 | this.log.info("Please do not place anything in this folder, it is under extension control."); 67 | fse.removeSync(polaris_cli_location); 68 | } 69 | 70 | this.log.info("Starting download.") 71 | const polaris_zip = path.join(polaris_install_path, "polaris.zip"); 72 | await this.polaris_service.download_cli(cli_url, polaris_zip); 73 | 74 | this.log.info("Starting extraction.") 75 | var zip = new zipper(polaris_zip); 76 | await zip.extractAllTo(polaris_cli_location, /*overwrite*/ true); 77 | this.log.info("Download and extraction finished.") 78 | 79 | fse.ensureFileSync(version_file); 80 | fs.writeFileSync(version_file, available_version_date.format(), 'utf8'); 81 | this.log.info(`Wrote version file: ${version_file}`) 82 | } 83 | 84 | this.log.info("Looking for Polaris executable.") 85 | var polaris_exe = await this.executable_finder.find_executable(polaris_cli_location); 86 | this.log.info("Found executable: " + polaris_exe) 87 | return new PolarisInstall(polaris_exe, polaris_home); 88 | } 89 | } -------------------------------------------------------------------------------- /src/gitlab/discussions.ts: -------------------------------------------------------------------------------- 1 | import {DiscussionSchema} from "@gitbeaker/core/dist/types/templates/ResourceDiscussions"; 2 | import {Gitlab} from "@gitbeaker/node"; 3 | import {logger} from "../SIGLogger"; 4 | import axios from "axios"; 5 | 6 | export async function gitlabGetDiscussions(gitlab_url: string, gitlab_token: string, project_id: string, merge_request_iid: number): Promise { 7 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 8 | 9 | logger.debug(`Getting merge request #${merge_request_iid} in project #${project_id}`) 10 | logger.debug(`GITLAB_TOKEN=${gitlab_token} GITLAB_URL=${gitlab_url}`) 11 | let merge_request = await api.MergeRequests.show(project_id, merge_request_iid) 12 | logger.debug(`Merge Request title is ${merge_request.title}`) 13 | 14 | logger.debug(`Merge request SHA is ${merge_request.sha}`) 15 | 16 | let discussions = await api.MergeRequestDiscussions.all(project_id, merge_request_iid) 17 | for (const discussion of discussions) { 18 | logger.debug(`Discussion ${discussion.id}`) 19 | if (discussion.notes) { 20 | for (const note of discussion.notes) { 21 | logger.debug(` body=${note.body}`) 22 | logger.debug(` base_sha=${note.position?.base_sha} head_sha=${note.position?.head_sha} start_sha=${note.position?.start_sha}`) 23 | logger.debug(` position_type=${note.position?.position_type} new_path=${note.position?.new_path} old_path=${note.position?.old_path}`) 24 | logger.debug(` new_line=${note.position?.new_line}`) 25 | } 26 | } 27 | } 28 | 29 | return discussions 30 | } 31 | 32 | 33 | 34 | export async function gitlabUpdateNote(gitlab_url: string, gitlab_token: string, project_id: string, merge_request_iid: number, 35 | discussion_id: number, note_id: number, body: string): Promise { 36 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 37 | 38 | logger.debug(`Update discussion #${discussion_id} note #${note_id} for merge request #${merge_request_iid} in project #${project_id} `) 39 | logger.debug(`new body is: ${body}`) 40 | 41 | await api.MergeRequestDiscussions.editNote(project_id, merge_request_iid, discussion_id, note_id, { body: body }) 42 | } 43 | 44 | export async function gitlabCreateDiscussionWithoutPosition(gitlab_url: string, gitlab_token: string, 45 | project_id: string, merge_request_iid: number, 46 | body: string): Promise { 47 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 48 | 49 | logger.debug(`Create new discussion for merge request #${merge_request_iid} in project #${project_id}`) 50 | 51 | await api.MergeRequestDiscussions.create(project_id, merge_request_iid, body) 52 | } 53 | 54 | export async function gitlabCreateDiscussion(gitlab_url: string, gitlab_token: string, project_id: string, merge_request_iid: number, 55 | line: number, filename: string, body: string, base_sha: string, commit_sha: string): Promise { 56 | 57 | // This implementation comes from GitBeaker, but does not work in all cases 58 | /* 59 | 60 | const api = new Gitlab({ host: gitlab_url, token: gitlab_token }) 61 | 62 | let merge_request = await api.MergeRequests.show(project_id, merge_request_iid) 63 | 64 | logger.debug(`XX Create new discussion for merge request #${merge_request_iid} in project #${project_id}`) 65 | 66 | await api.MergeRequestDiscussions.create(project_id, merge_request_iid, body, { 67 | position: { 68 | position_type: "text", 69 | base_sha: base_sha, 70 | head_sha: merge_request.sha, 71 | start_sha: base_sha, 72 | new_path: filename, 73 | old_path: filename, 74 | new_line: line.toString() 75 | } 76 | }) 77 | */ 78 | 79 | // JC: GitBeaker isn't working for this case (filed https://github.com/jdalrymple/gitbeaker/issues/2396) 80 | // Working around using bare REST query 81 | 82 | const FormData = require('form-data'); 83 | const formData = new FormData(); 84 | formData.append("body", body) 85 | formData.append("position[position_type]", "text") 86 | formData.append("position[base_sha]", base_sha) 87 | formData.append("position[start_sha]", base_sha) 88 | //formData.append("position[head_sha]", merge_request.sha) 89 | formData.append("position[head_sha]", commit_sha) 90 | formData.append("position[new_path]", filename) 91 | formData.append("position[old_path]", filename) 92 | formData.append("position[new_line]", line.toString()) 93 | 94 | let headers = { 95 | "PRIVATE-TOKEN": gitlab_token, 96 | 'content-type': `multipart/form-data; boundary=${formData._boundary}` 97 | } 98 | 99 | logger.debug(`headers=${headers}`) 100 | 101 | let url = `${gitlab_url}/api/v4/projects/${project_id}/merge_requests/${merge_request_iid}/discussions` 102 | 103 | logger.debug(`url=${url}`) 104 | 105 | let res = undefined 106 | try { 107 | res = await axios.post(url, 108 | formData, { 109 | headers: headers 110 | }) 111 | 112 | logger.debug(`res=${res.status} res=${res.data} status=${res.statusText} h=${res.headers}`) 113 | 114 | if (res.status > 201) { 115 | logger.error(`Unable to create discussion for ${filename}:${line} at ${url}`) 116 | logger.debug(`ERROR`) 117 | return 118 | } 119 | 120 | } catch (error: any) { 121 | // we'll proceed, but let's report it 122 | logger.error(`${error.message}`) 123 | } 124 | 125 | logger.debug(`OK`) 126 | 127 | return 128 | } -------------------------------------------------------------------------------- /src/blackduck/blackduck-api.ts: -------------------------------------------------------------------------------- 1 | import { IHeaders } from 'typed-rest-client/Interfaces' 2 | import { BearerCredentialHandler } from 'typed-rest-client/Handlers' 3 | import { HttpClient } from 'typed-rest-client/HttpClient' 4 | import { IRestResponse, RestClient } from 'typed-rest-client/RestClient' 5 | import {logger} from "../SIGLogger"; 6 | 7 | const APPLICATION_NAME = "Synopsys SIG Library for Node.js" 8 | 9 | export interface IBlackduckView { 10 | _meta: { 11 | href: string 12 | } 13 | } 14 | 15 | export interface IBlackduckItemArray extends IBlackduckView { 16 | totalCount: number 17 | items: Array 18 | } 19 | 20 | export interface IUpgradeGuidance { 21 | version: string 22 | shortTerm: IRecommendedVersion 23 | longTerm: IRecommendedVersion 24 | } 25 | 26 | export interface IRecommendedVersion { 27 | version: string 28 | versionName: string 29 | vulnerabilityRisk: Object 30 | } 31 | 32 | export interface IComponentSearchResult { 33 | version: string 34 | } 35 | 36 | export interface IComponentVersion { 37 | license: { 38 | licenses: { 39 | license: string 40 | name: string 41 | }[] 42 | } 43 | _meta: { 44 | href: string 45 | } 46 | } 47 | 48 | export interface IComponentVulnerability { 49 | name: string 50 | severity: string 51 | useCvss3: boolean 52 | cvss2: ICvssView 53 | cvss3: ICvssView 54 | _meta: { 55 | href: string 56 | } 57 | } 58 | 59 | export interface ICvssView { 60 | baseScore: number 61 | severity: string 62 | } 63 | 64 | export interface IRapidScanResults { 65 | componentName: string 66 | versionName: string 67 | componentIdentifier: string 68 | violatingPolicyNames: string[] 69 | policyViolationVulnerabilities: IRapidScanVulnerability[] 70 | policyViolationLicenses: IRapidScanLicense[] 71 | _meta: { 72 | href: string 73 | } 74 | } 75 | 76 | export interface IRapidScanVulnerability { 77 | name: string 78 | } 79 | 80 | export interface IRapidScanLicense { 81 | licenseName: string 82 | _meta: { 83 | href: string 84 | } 85 | } 86 | 87 | export class BlackduckApiService { 88 | blackduckUrl: string 89 | blackduckApiToken: string 90 | 91 | constructor(blackduckUrl: string, blackduckApiToken: string) { 92 | this.blackduckUrl = cleanUrl(blackduckUrl) 93 | this.blackduckApiToken = blackduckApiToken 94 | } 95 | 96 | async getBearerToken(): Promise { 97 | logger.info('Initiating authentication request to Black Duck...') 98 | const authenticationClient = new HttpClient(APPLICATION_NAME) 99 | const authorizationHeader: IHeaders = { Authorization: `token ${this.blackduckApiToken}` } 100 | 101 | return authenticationClient 102 | .post(`${this.blackduckUrl}/api/tokens/authenticate`, '', authorizationHeader) 103 | .then(authenticationResponse => authenticationResponse.readBody()) 104 | .then(responseBody => JSON.parse(responseBody)) 105 | .then(responseBodyJson => { 106 | logger.info('Successfully authenticated with Black Duck') 107 | return responseBodyJson.bearerToken 108 | }) 109 | } 110 | 111 | async checkIfEnabledBlackduckPoliciesExist(bearerToken: string): Promise { 112 | logger.debug('Requesting policies from Black Duck...') 113 | return this.getPolicies(bearerToken, 1, true).then(blackduckPolicyPage => { 114 | const policyCount = blackduckPolicyPage?.result?.totalCount 115 | if (policyCount === undefined || policyCount === null) { 116 | logger.warn('Failed to check Black Duck for policies') 117 | return false 118 | } else if (policyCount > 0) { 119 | logger.debug(`${policyCount} Black Duck policies existed`) 120 | return true 121 | } else { 122 | logger.info('No Black Duck policies exist') 123 | return false 124 | } 125 | }) 126 | } 127 | 128 | async getUpgradeGuidanceFor(bearerToken: string, componentVersion: IComponentVersion): Promise> { 129 | return this.get(bearerToken, `${componentVersion._meta.href}/upgrade-guidance`) 130 | } 131 | 132 | async getComponentsMatching(bearerToken: string, componentIdentifier: string, limit: number = 10): Promise>> { 133 | const requestPath = `/api/components?q=${componentIdentifier}` 134 | 135 | return this.requestPage(bearerToken, requestPath, 0, limit) 136 | } 137 | 138 | async getComponentVersion(bearerToken: string, searchResult: IComponentSearchResult) { 139 | return this.get(bearerToken, searchResult.version) 140 | } 141 | 142 | async getComponentVersionMatching(bearerToken: string, componentIdentifier: string, limit: number = 10): Promise { 143 | const componentSearchResponse = await this.getComponentsMatching(bearerToken, componentIdentifier, limit) 144 | const firstMatchingComponentVersionUrl = componentSearchResponse?.result?.items[0].version 145 | 146 | let componentVersion = null 147 | if (firstMatchingComponentVersionUrl !== undefined) { 148 | const componentVersionResponse: IRestResponse = await this.get(bearerToken, firstMatchingComponentVersionUrl) 149 | componentVersion = componentVersionResponse?.result 150 | } 151 | 152 | return componentVersion 153 | } 154 | 155 | async getComponentVulnerabilties(bearerToken: string, componentVersion: IComponentVersion): Promise>> { 156 | return this.get(bearerToken, `${componentVersion._meta.href}/vulnerabilities`, 'application/vnd.blackducksoftware.vulnerability-4+json') 157 | } 158 | 159 | async getPolicies(bearerToken: string, limit: number = 10, enabled?: boolean) { 160 | const enabledFilter = enabled === undefined || enabled === null ? '' : `filter=policyRuleEnabled%3A${enabled}` 161 | const requestPath = `/api/policy-rules?${enabledFilter}` 162 | 163 | return this.requestPage(bearerToken, requestPath, 0, limit) 164 | } 165 | 166 | async requestPage(bearerToken: string, requestPath: string, offset: number, limit: number): Promise>> { 167 | return this.get(bearerToken, `${this.blackduckUrl}${requestPath}&offset=${offset}&limit=${limit}`) 168 | } 169 | 170 | async get(bearerToken: string, requestUrl: string, acceptHeader?: string): Promise> { 171 | const bearerTokenHandler = new BearerCredentialHandler(bearerToken, true) 172 | const blackduckRestClient = new RestClient(APPLICATION_NAME, this.blackduckUrl, [bearerTokenHandler]) 173 | 174 | return blackduckRestClient.get(requestUrl, { acceptHeader }) 175 | } 176 | } 177 | 178 | export function cleanUrl(blackduckUrl: string) { 179 | if (blackduckUrl && blackduckUrl.endsWith('/')) { 180 | return blackduckUrl.substr(0, blackduckUrl.length - 1) 181 | } 182 | return blackduckUrl 183 | } 184 | -------------------------------------------------------------------------------- /src/polaris/service/PolarisService.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as os from 'os'; 3 | var ProxyAgent = require("proxy-agent"); 4 | const HttpsProxyAgent = require("https-proxy-agent"); 5 | const url = require('url'); 6 | const Axios = require('axios'); 7 | const moment = require("moment"); 8 | const CancelToken = Axios.CancelToken; 9 | const fs = require('fs'); 10 | const json_path = require('jsonpath'); 11 | const debug = require('debug'); 12 | import PolarisConnection from "../model/PolarisConnection"; 13 | 14 | export default class PolarisService { 15 | log: any; 16 | polaris_url: string; 17 | access_token: string; 18 | bearer_token: string | null; 19 | headers: any | null; 20 | axios: any; 21 | constructor(log:any, connection: PolarisConnection) { 22 | if (connection.url.endsWith("/") || connection.url.endsWith("\\")) { 23 | this.polaris_url = connection.url.slice(0, -1); 24 | } else { 25 | this.polaris_url = connection.url; 26 | } 27 | 28 | this.access_token = connection.token; 29 | this.bearer_token = null; 30 | this.headers = null; 31 | this.log = log; 32 | 33 | 34 | if (connection.proxy != undefined) { 35 | log.info(`Using Proxy URL: ${connection.proxy.proxy_url}`) 36 | var proxyOpts = url.parse(connection.proxy.proxy_url); 37 | 38 | var proxyConfig :any = { 39 | host: proxyOpts.hostname, 40 | port: proxyOpts.port 41 | }; 42 | 43 | if (connection.proxy.proxy_username && connection.proxy.proxy_password) { 44 | log.info("Using configured proxy credentials.") 45 | proxyConfig.auth = connection.proxy.proxy_username + ":" + connection.proxy.proxy_password; 46 | } 47 | 48 | const httpsAgent = new HttpsProxyAgent(proxyConfig) 49 | this.axios = Axios.create({httpsAgent}); 50 | } else { 51 | this.axios = Axios.create(); 52 | } 53 | } 54 | 55 | async authenticate() { 56 | this.log.info("Authenticating with Polaris Software Integrity Platform.") 57 | debug.enable('https-proxy-agent'); 58 | this.bearer_token = await this.fetch_bearer_token(); 59 | debug.disable(); 60 | this.headers = { 61 | Authorization: `Bearer ${this.bearer_token}` 62 | } 63 | } 64 | 65 | async get_job(job_status_url: string) { 66 | return await this.axios.get(job_status_url, { 67 | headers: this.headers 68 | }); 69 | } 70 | 71 | fetch_bearer_token():Promise { 72 | // this is a workaround for https://github.com/TooTallNate/node-https-proxy-agent/issues/102 73 | //basically NodeJS thinks all event loops are closed, this ensures the event look hasn't closed. 74 | // TODO: Need to switch to a new http library that doesn't suffer from this bug. 75 | //Basically we need to reject the promise ourselves 76 | const resultPromise = new Promise((resolve, reject) => { 77 | const timeout = 10000 78 | setTimeout(() => { reject(new Error(`Failed to authenticate with Polaris Software Integrity Platform. This may be a problem with your url, proxy setup or network.`))}, timeout); 79 | 80 | var authenticateUrl = this.polaris_url + "/api/auth/authenticate"; 81 | 82 | try { 83 | this.axios.post(authenticateUrl, "accesstoken=" + this.access_token, {timeout: 10000, headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then((authResponse:any) => { 84 | if (authResponse.data.jwt) { 85 | this.log.info("Succesfully authenticated, saving bearer token.") 86 | resolve(authResponse.data.jwt); 87 | } else { 88 | this.log.error(`Failed to authenticate with Polaris Software Integrity Platform, no bearer token received.`) 89 | reject(new Error(`Failed to authenticate with Polaris Software Integrity Platform. Status: ${authResponse.status} Reason: ${authResponse.statusText}`)) 90 | } 91 | }).catch((e:any) => { 92 | this.log.error(`Unable to authenticate with Polaris Software Integrity Platform at url: ${authenticateUrl}`); 93 | this.log.error(`This may be a problem with your Polaris Software Integrity Platform url, proxy setup or network.`); 94 | reject(e); 95 | }) 96 | } catch (e) { 97 | this.log.error(`Unable to authenticate with Polaris Software Integrity Platform at url: ${authenticateUrl}`); 98 | this.log.error(`This may be a problem with your Polaris Software Integrity Platform url, proxy setup or network.`); 99 | reject(e); 100 | } 101 | }) 102 | return resultPromise; 103 | } 104 | 105 | async fetch_cli_modified_date(url: string): Promise { //return type should be a moment 106 | var token = CancelToken.source(); 107 | var self = this; 108 | self.log.debug("Fetching cli modified date from: " + url); 109 | return new Promise((resolve, reject) => { 110 | this.axios({ 111 | url: url, 112 | method: 'GET', 113 | responseType: 'stream', // important, let's us cancel the after we get the headers. 114 | cancelToken: token.token 115 | }).then(function (response: any) { 116 | var lastModifiedText = response.headers['last-modified']; 117 | self.log.debug("Last Modified Header: " + lastModifiedText); 118 | var lastModifiedDate = moment(lastModifiedText); 119 | self.log.debug("Last Modified Date: " + lastModifiedDate.format()); 120 | token.cancel(); 121 | resolve(lastModifiedDate); 122 | }).catch(function (error: any) { 123 | reject(error); 124 | }); 125 | }); 126 | } 127 | 128 | async fetch_issue_data(url: string): Promise { 129 | return await this.axios.get(url, { 130 | headers: this.headers 131 | }); 132 | } 133 | 134 | async get_url(url: string): Promise { 135 | return await this.axios.get(url, { 136 | headers: this.headers 137 | }); 138 | } 139 | 140 | async fetch_organization_name(): Promise { 141 | var target = this.polaris_url + "/api/auth/contexts"; 142 | var result = await this.axios({ 143 | url: target, 144 | method: 'GET', 145 | responseType: 'json', 146 | headers: this.headers, 147 | }); 148 | var organization_names = json_path.query(result.data, "$.data[*].attributes.organizationname"); 149 | if (organization_names.length > 0) { 150 | return organization_names[0]; 151 | } else { 152 | return null; 153 | } 154 | } 155 | 156 | async download_cli(url: string, file: string) { 157 | this.log.debug("Downloading cli from: " + url); 158 | this.log.debug("Downloading cli to: " + file); 159 | 160 | const writer = fs.createWriteStream(file); 161 | 162 | const response = await this.axios({ 163 | url, 164 | method: 'GET', 165 | responseType: 'stream' 166 | }); 167 | 168 | response.data.pipe(writer); 169 | 170 | return new Promise((resolve, reject) => { 171 | writer.on('finish', resolve); 172 | writer.on('error', reject) 173 | }); 174 | } 175 | } -------------------------------------------------------------------------------- /src/coverity/coverity-utils.ts: -------------------------------------------------------------------------------- 1 | import {CoverityIssueOccurrence} from "../models/coverity-json-v7-schema"; 2 | import {logger} from "../SIGLogger"; 3 | import {DiffMap} from "../diffmap"; 4 | import {relatavize_path} from "../paths"; 5 | import fs from "fs"; 6 | 7 | export const COVERITY_PRESENT = 'PRESENT' 8 | export const COVERITY_NOT_PRESENT = 'NOT_PRESENT' 9 | export const COVERITY_UNKNOWN_FILE = 'Unknown File' 10 | export const COVERITY_COMMENT_PREFACE = ' 23 | 24 | Coverity issue no longer present as of: ${process.env.CI_COMMIT_SHA} 25 |
26 | Show issue 27 | 28 | ${existingMessageLines.slice(4).join('\n')} 29 |
` 30 | } 31 | 32 | export function coverityCreateReviewCommentMessage(issue: CoverityIssueOccurrence): string { 33 | const issueName = issue.checkerProperties ? issue.checkerProperties.subcategoryShortDescription : issue.checkerName 34 | const checkerNameString = issue.checkerProperties ? `\r\n_${issue.checkerName}_` : '' 35 | const impactString = issue.checkerProperties ? issue.checkerProperties.impact : 'Unknown' 36 | const cweString = issue.checkerProperties ? `, CWE-${issue.checkerProperties.cweCategory}` : '' 37 | const mainEvent = issue.events.find(event => event.main) 38 | const mainEventDescription = mainEvent ? mainEvent.eventDescription : '' 39 | const remediationEvent = issue.events.find(event => event.remediation) 40 | const remediationString = remediationEvent ? `## How to fix\r\n ${remediationEvent.eventDescription}` : '' 41 | 42 | return `${COVERITY_COMMENT_PREFACE} 43 | ${issue.mergeKey} 44 | ${COVERITY_PRESENT} 45 | --> 46 | 47 | # Coverity Issue - ${issueName} 48 | ${mainEventDescription} 49 | 50 | _${impactString} Impact${cweString}_${checkerNameString} 51 | 52 | ${remediationString} 53 | ` 54 | } 55 | 56 | export function coverityCreateIssueCommentMessage(issue: CoverityIssueOccurrence, file_link: string): string { 57 | const message = coverityCreateReviewCommentMessage(issue) 58 | const relativePath = issue.strippedMainEventFilePathname.startsWith('/') ? 59 | relatavize_path(process.cwd(), issue.strippedMainEventFilePathname) : 60 | issue.strippedMainEventFilePathname 61 | 62 | return `${message} 63 | ## Issue location 64 | This issue was discovered outside the diff for this Pull Request. You can find it at: 65 | [${relativePath}:${issue.mainEventLineNumber}](${file_link}) 66 | ` 67 | } 68 | 69 | export function coverityIsInDiff(issue: CoverityIssueOccurrence, diffMap: DiffMap): boolean { 70 | logger.debug(`Look for issue: ${issue.strippedMainEventFilePathname}:${issue.mainEventLineNumber}`) 71 | /* 72 | const relativePath = issue.strippedMainEventFilePathname.startsWith('/') ? 73 | relatavize_path(process.cwd(), issue.strippedMainEventFilePathname) : 74 | issue.strippedMainEventFilePathname 75 | 76 | */ 77 | 78 | const diffHunks = diffMap.get(issue.strippedMainEventFilePathname) 79 | logger.debug(`diffHunks=${diffHunks}`) 80 | 81 | if (!diffHunks) { 82 | return false 83 | } 84 | 85 | for (const hunk of diffHunks) { 86 | logger.debug(`hunk for ${issue.strippedMainEventFilePathname}: ${hunk.firstLine},${hunk.lastLine}`) 87 | } 88 | 89 | return diffHunks.filter(hunk => hunk.firstLine <= issue.mainEventLineNumber).some(hunk => issue.mainEventLineNumber <= hunk.lastLine) 90 | } 91 | 92 | export function coverityCreateIssueEvidence(issue: CoverityIssueOccurrence) { 93 | // Will create a map from files to lines to list of events and code snippets 94 | let event_tree_lines = new Map>() 95 | let event_tree_events = new Map>>() 96 | let evidence = '' 97 | 98 | // Loop through each event and collect source code artifacts 99 | for (const event of issue.events) { 100 | const event_file = event.strippedFilePathname 101 | const event_line = event.lineNumber 102 | 103 | //logger.info(`Event file=${event_file} line=${event_line} ${event.eventNumber}`) 104 | if (!event_tree_lines.get(event_file)) { 105 | event_tree_lines.set(event_file, new Map()) 106 | event_tree_events.set(event_file, new Map>()) 107 | } 108 | 109 | // Collect +/- 3 lines of code 110 | let event_line_start = event_line - 3 111 | if (event_line_start < 0) { 112 | event_line_start = 0 113 | } 114 | let event_line_end = event_line + 3 115 | 116 | for (let i = event_line_start; i < event_line_end; i ++) { 117 | if (!event_tree_lines.get(event_file)) { logger.debug(`Not set!`) } 118 | event_tree_lines.get(event_file)?.set(i, 1) 119 | } 120 | 121 | if (!event_tree_events.get(event_file)?.get(event_line)) { 122 | event_tree_events.get(event_file)?.set(event_line, []) 123 | } 124 | event_tree_events.get(event_file)?.get(event_line)?.push( 125 | `${event.eventNumber}. ${event.eventTag}: ${event.eventDescription}`) 126 | //logger.debug(`Push: ${event.eventNumber}. ${event.eventTag}: ${event.eventDescription}`) 127 | } 128 | 129 | let keys = Array.from( event_tree_lines.keys() ); 130 | for (const filename of keys) { 131 | evidence += `\n**From ${filename}:**\n\n` 132 | evidence += "```\n" 133 | 134 | const event_to_lines = event_tree_lines.get(filename) 135 | if (event_to_lines) { 136 | let keys = Array.from(event_to_lines.keys()) 137 | for (const i of keys) { 138 | if (event_tree_events.get(filename)?.has(i)) { 139 | let events_and_lines = event_tree_events.get(filename)?.get(i) 140 | if (events_and_lines) { 141 | for (const event_str of events_and_lines) { 142 | evidence += `${event_str}\n` 143 | } 144 | } 145 | } 146 | 147 | const code_line = get_line(filename, i) 148 | const line_string = i.toString().padStart(5, '0') 149 | evidence += `${line_string} ${code_line}\n` 150 | } 151 | } 152 | } 153 | evidence += "```\n" 154 | 155 | return evidence 156 | } 157 | 158 | function get_line(filename: string, line_no: number): string { 159 | const data = fs.readFileSync(filename, 'utf8'); 160 | const lines = data.split('\n'); 161 | 162 | if (+line_no > lines.length) { 163 | throw new Error('File end reached without finding line') 164 | } 165 | 166 | return lines[+line_no] 167 | } 168 | 169 | export function coverityCreateIssue(issue: CoverityIssueOccurrence): string { 170 | const issueName = issue.checkerProperties ? issue.checkerProperties.subcategoryShortDescription : issue.checkerName 171 | const checkerNameString = issue.checkerProperties ? `\r\n_${issue.checkerName}_` : '' 172 | const impactString = issue.checkerProperties ? issue.checkerProperties.impact : 'Unknown' 173 | const cweString = issue.checkerProperties ? `, CWE-${issue.checkerProperties.cweCategory}` : '' 174 | const mainEvent = issue.events.find(event => event.main) 175 | const mainEventDescription = mainEvent ? mainEvent.eventDescription : '' 176 | const remediationEvent = issue.events.find(event => event.remediation) 177 | const remediationString = remediationEvent ? `## How to fix\r\n ${remediationEvent.eventDescription}` : '' 178 | const issue_evidence = coverityCreateIssueEvidence(issue) 179 | 180 | return ` 181 | # Coverity Issue - ${issueName} 182 | ${mainEventDescription} 183 | 184 | _${impactString} Impact${cweString}_ ${checkerNameString} 185 | 186 | ${remediationString} 187 | 188 | ${issue_evidence} 189 | ` 190 | } -------------------------------------------------------------------------------- /src/polaris/model/PolarisAPI.ts: -------------------------------------------------------------------------------- 1 | export interface IPolarisRunData { 2 | data: IPolarisRun[], 3 | "meta": { 4 | "offset": number, 5 | "limit": number, 6 | "total": number 7 | } 8 | } 9 | 10 | export interface IPolarisRun { 11 | type: string, 12 | id: string, 13 | attributes: { 14 | "fingerprints": string, 15 | "upload-id": string, 16 | "run-type": string, 17 | "creation-date": string, 18 | "completed-date": string, 19 | "status": string, 20 | "segment": string 21 | }, 22 | relationships: { 23 | "project"?: { 24 | links: IPolarisRelationshipLink, 25 | data: IPolarisRelationshipData 26 | }, 27 | "tool"?: { 28 | links: IPolarisRelationshipLink, 29 | data: IPolarisRelationshipData 30 | }, 31 | "previous-run"?: { 32 | links: IPolarisRelationshipLink, 33 | data: IPolarisRelationshipData 34 | }, 35 | "properties"?: { 36 | links: IPolarisRelationshipLink, 37 | data: IPolarisRelationshipData 38 | }, 39 | "revision"?: { 40 | links: IPolarisRelationshipLink, 41 | data: IPolarisRelationshipData 42 | }, 43 | "tool-domain-service"?: { 44 | links: IPolarisRelationshipLink, 45 | data: IPolarisRelationshipData 46 | }, 47 | "submitting-organization"?: { 48 | links: IPolarisRelationshipLink, 49 | data: IPolarisRelationshipData 50 | }, 51 | "submitting-user"?: { 52 | links: IPolarisRelationshipLink, 53 | data: IPolarisRelationshipData 54 | }, 55 | } 56 | links: { 57 | self: IPolarisHref 58 | }, 59 | meta: { 60 | "etag": string, 61 | "organization-id": string, 62 | "in-trash": boolean 63 | } 64 | } 65 | 66 | export interface IPolarisBranchData { 67 | data: IPolarisBranch[], 68 | "meta": { 69 | "offset": number, 70 | "limit": number, 71 | "total": number 72 | } 73 | } 74 | 75 | export interface IPolarisBranch { 76 | "type": string, 77 | "id": string, 78 | "attributes": { 79 | "name": string, 80 | "main-for-project": boolean 81 | }, 82 | relationships: { 83 | "revisions"?: { 84 | links: IPolarisRelationshipLink, 85 | data: IPolarisRelationshipData 86 | }, 87 | "project"?: { 88 | links: IPolarisRelationshipLink, 89 | data: IPolarisRelationshipData 90 | } 91 | } 92 | links: { 93 | self: IPolarisHref 94 | }, 95 | meta: { 96 | "etag": string, 97 | "organization-id": string, 98 | "in-trash": boolean 99 | } 100 | } 101 | 102 | export interface IPolarisIssueDataReturn { 103 | issueData: IPolarisIssue[], 104 | issueIncluded: IPolarisIssueIncluded[] 105 | } 106 | 107 | export interface IPolarisIssueData { 108 | data: IPolarisIssue[], 109 | included: IPolarisIssueIncluded[], 110 | meta: { 111 | "total": number, 112 | "offset": number, 113 | "limit": number, 114 | "complete": true, 115 | "run-count": number, 116 | "latest-run-ids": string[] 117 | } 118 | } 119 | 120 | export interface IPolarisIssue { 121 | type: string, 122 | id: string, 123 | attributes: { 124 | "finding-key": string, 125 | "issue-key": string, 126 | "sub-tool": string, 127 | "severity": string 128 | }, 129 | relationships: { 130 | "path"?: { 131 | data: IPolarisRelationshipData 132 | }, 133 | "tool-domain-service"?: { 134 | data: IPolarisRelationshipData 135 | }, 136 | "reachability"?: { 137 | data: IPolarisRelationshipData 138 | }, 139 | "issue-type"?: { 140 | data: IPolarisRelationshipData 141 | }, 142 | "tool"?: { 143 | data: IPolarisRelationshipData 144 | }, 145 | "latest-observed-on-run"?: { 146 | data: IPolarisRelationshipData 147 | }, 148 | "transitions"?: { 149 | data: IPolarisRelationshipData[] 150 | }, 151 | "related-taxa"?: { 152 | data: IPolarisRelationshipData[] 153 | }, 154 | "related-indicators"?: { 155 | data: IPolarisRelationshipData[] 156 | }, 157 | "severity"?: { 158 | data: IPolarisRelationshipData 159 | } 160 | }, 161 | links: { 162 | self: IPolarisHref 163 | } 164 | } 165 | 166 | export interface IPolarisIssueIncluded { 167 | "id": string, 168 | "type": string, 169 | "attributes": { 170 | "transition-type"?: string, 171 | "cause"?: string, 172 | "human-readable-cause"?: string, 173 | "transition-date"?: string, 174 | "branch-id"?: string, 175 | "revision-id"?: string, 176 | "run-id"?: string, 177 | "issue-type"?: string, 178 | "name"?: string, 179 | "description"?: string, 180 | "abbreviation"?: string, 181 | "local-effect"?: string, 182 | "path"?: string[] 183 | } 184 | } 185 | 186 | export interface IPolarisIssueTriageData { 187 | data: IPolarisIssueTriage 188 | } 189 | 190 | export interface IPolarisIssueTriage { 191 | type: string, 192 | id: string, 193 | links: { 194 | self: IPolarisHref 195 | }, 196 | attributes: { 197 | "issue-key": string, 198 | "project-id": string, 199 | "triage-current-values": IPolarisIssueTriageValue[] 200 | } 201 | } 202 | 203 | export interface IPolarisIssueTriageValue { 204 | "attribute-semantic-id": string, 205 | "attribute-name": string, 206 | "kind": string, 207 | "value": string, 208 | "timestamp": string, 209 | "display-value": string 210 | } 211 | 212 | export async function getTriageValue(attribute_name: string, triage_values: IPolarisIssueTriageValue[]): 213 | Promise { 214 | for (const value of triage_values) { 215 | if (attribute_name == value["attribute-semantic-id"]) { 216 | return value 217 | } 218 | } 219 | return Promise.reject() 220 | } 221 | 222 | export interface IPolarisCodeAnalysisEventsData { 223 | data: IPolarisCodeAnalysisEvents[], 224 | meta: { 225 | "limit": number, 226 | "offset": number, 227 | "total": number 228 | } 229 | } 230 | 231 | export interface IPolarisCodeAnalysisEvents { 232 | "run-id": string, 233 | "finding-key": string, 234 | "main-event-file-path": string[], 235 | "main-event-line-number": number, 236 | language: string, 237 | "example-events-caption": string, 238 | "example-events-groups": string[], 239 | events: IPolarisCodeAnalysisEvent[], 240 | type: string 241 | } 242 | 243 | export interface IPolarisCodeAnalysisEvent { 244 | "covlstr-event-description": string, 245 | "event-description": string, 246 | "event-number": number, 247 | "event-set": number, 248 | "event-tag": string, 249 | "event-tree-position": string, 250 | "event-type": string, 251 | "line-number": number, 252 | "source-before": IPolarisCodeAnalysisEventSource, 253 | "source-after": IPolarisCodeAnalysisEventSource, 254 | "path": string[], 255 | "filePath": string, 256 | "evidence-events": IPolarisCodeAnalysisEvent[], 257 | } 258 | 259 | interface IPolarisCodeAnalysisEventSource { 260 | "start-line": number, 261 | "end-line": number, 262 | "source-code": string 263 | } 264 | 265 | interface IPolarisRelationshipLink { 266 | self: string, 267 | related: string 268 | } 269 | 270 | interface IPolarisRelationshipData { 271 | type: string, 272 | id: string 273 | } 274 | 275 | interface IPolarisHref { 276 | "href": string, 277 | "meta": { 278 | "durable": string 279 | } 280 | } 281 | 282 | export interface IPolarisIssueUnified { 283 | key: string, 284 | name: string, 285 | description: string, 286 | localEffect: string, 287 | checkerName: string, 288 | path: string, 289 | line: number, 290 | severity: string, 291 | cwe: string, 292 | mainEvent: string, 293 | mainEventDescription: string, 294 | remediationEvent: string, 295 | remediationEventDescription: string, 296 | dismissed: boolean, 297 | events: IPolarisIssueUnifiedEvent[], 298 | link: string 299 | } 300 | 301 | export interface IPolarisIssueUnifiedEvent { 302 | "description": string, 303 | "number": number, 304 | "tag": string, 305 | "type": string, 306 | "line-number": number, 307 | "source-before": IPolarisCodeAnalysisEventSource, 308 | "source-after": IPolarisCodeAnalysisEventSource, 309 | "filePath": string 310 | } -------------------------------------------------------------------------------- /src/blackduck/blackduck-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlackduckApiService, 3 | IComponentVersion, 4 | IComponentVulnerability, IRapidScanLicense, 5 | IRapidScanResults, IRapidScanVulnerability, IRecommendedVersion, 6 | IUpgradeGuidance 7 | } from "./blackduck-api"; 8 | import {logger} from "../SIGLogger"; 9 | 10 | export const TABLE_HEADER = '| Policies Violated | Dependency | License(s) | Vulnerabilities | Short Term Recommended Upgrade | Long Term Recommended Upgrade |\r\n' + '|-|-|-|-|-|-|\r\n' 11 | 12 | export async function createRapidScanReportString(blackduck_url: string, blackduck_api_token: string, policyViolations: IRapidScanResults[], policyCheckWillFail: boolean): Promise { 13 | let message = '' 14 | if (policyViolations.length == 0) { 15 | message = message.concat('# :white_check_mark: None of your dependencies violate policy!') 16 | } else { 17 | const violationSymbol = policyCheckWillFail ? ':x:' : ':warning:' 18 | message = message.concat(`# ${violationSymbol} Found dependencies violating policy!\r\n\r\n`) 19 | 20 | const componentReports = await createRapidScanReport(blackduck_url, blackduck_api_token, policyViolations) 21 | const tableBody = componentReports.map(componentReport => createComponentRow(componentReport)).join('\r\n') 22 | const reportTable = TABLE_HEADER.concat(tableBody) 23 | message = message.concat(reportTable) 24 | } 25 | 26 | return message 27 | } 28 | 29 | function createComponentRow(component: IComponentReport): string { 30 | const violatedPolicies = component.violatedPolicies.join('
') 31 | const componentInViolation = component?.href ? `[${component.name}](${component.href})` : component.name 32 | const componentLicenses = component.licenses.map(license => `${license.violatesPolicy ? ':x:   ' : ''}[${license.name}](${license.href})`).join('
') 33 | const vulnerabilities = component.vulnerabilities.map(vulnerability => `${vulnerability.violatesPolicy ? ':x:   ' : ''}[${vulnerability.name}](${vulnerability.href})${vulnerability.cvssScore && vulnerability.severity ? ` ${vulnerability.severity}: CVSS ${vulnerability.cvssScore}` : ''}`).join('
') 34 | const shortTermString = component.shortTermUpgrade ? `[${component.shortTermUpgrade.name}](${component.shortTermUpgrade.href}) (${component.shortTermUpgrade.vulnerabilityCount} known vulnerabilities)` : '' 35 | const longTermString = component.longTermUpgrade ? `[${component.longTermUpgrade.name}](${component.longTermUpgrade.href}) (${component.longTermUpgrade.vulnerabilityCount} known vulnerabilities)` : '' 36 | 37 | return `| ${violatedPolicies} | ${componentInViolation} | ${componentLicenses} | ${vulnerabilities} | ${shortTermString} | ${longTermString} |` 38 | } 39 | 40 | export async function createRapidScanReport(blackduck_url: string, blackduck_api_token: string, policyViolations: IRapidScanResults[], blackduckApiService?: BlackduckApiService): Promise { 41 | const rapidScanReport: IComponentReport[] = [] 42 | 43 | if (blackduckApiService === undefined) { 44 | blackduckApiService = new BlackduckApiService(blackduck_url, blackduck_api_token) 45 | } 46 | 47 | const bearerToken = await blackduckApiService.getBearerToken() 48 | 49 | for (const policyViolation of policyViolations) { 50 | const componentIdentifier = policyViolation.componentIdentifier 51 | const componentVersion = await blackduckApiService.getComponentVersionMatching(bearerToken, componentIdentifier) 52 | 53 | let upgradeGuidance = undefined 54 | let vulnerabilities = undefined 55 | if (componentVersion !== null) { 56 | upgradeGuidance = await blackduckApiService 57 | .getUpgradeGuidanceFor(bearerToken, componentVersion) 58 | .then(response => { 59 | if (response.result === null) { 60 | logger.warn(`Could not get upgrade guidance for ${componentIdentifier}: The upgrade guidance result was empty`) 61 | return undefined 62 | } 63 | 64 | return response.result 65 | }) 66 | .catch(reason => { 67 | logger.warn(`Could not get upgrade guidance for ${componentIdentifier}: ${reason}`) 68 | return undefined 69 | }) 70 | 71 | const vulnerabilityResponse = await blackduckApiService.getComponentVulnerabilties(bearerToken, componentVersion) 72 | vulnerabilities = vulnerabilityResponse?.result?.items 73 | } 74 | 75 | const componentVersionOrUndefined = componentVersion === null ? undefined : componentVersion 76 | const componentReport = createComponentReport(policyViolation, componentVersionOrUndefined, upgradeGuidance, vulnerabilities) 77 | rapidScanReport.push(componentReport) 78 | } 79 | 80 | return rapidScanReport 81 | } 82 | export interface IComponentReport { 83 | violatedPolicies: string[] 84 | name: string 85 | href?: string 86 | licenses: ILicenseReport[] 87 | vulnerabilities: IVulnerabilityReport[] 88 | shortTermUpgrade?: IUpgradeReport 89 | longTermUpgrade?: IUpgradeReport 90 | } 91 | 92 | export function createComponentReport(violation: IRapidScanResults, componentVersion?: IComponentVersion, upgradeGuidance?: IUpgradeGuidance, vulnerabilities?: IComponentVulnerability[]): IComponentReport { 93 | return { 94 | violatedPolicies: violation.violatingPolicyNames, 95 | name: `${violation.componentName} ${violation.versionName}`, 96 | href: componentVersion?._meta.href, 97 | licenses: createComponentLicenseReports(violation.policyViolationLicenses, componentVersion), 98 | vulnerabilities: createComponentVulnerabilityReports(violation.policyViolationVulnerabilities, vulnerabilities), 99 | shortTermUpgrade: createUpgradeReport(upgradeGuidance?.shortTerm), 100 | longTermUpgrade: createUpgradeReport(upgradeGuidance?.longTerm) 101 | } 102 | } 103 | 104 | export function createComponentLicenseReports(policyViolatingLicenses: IRapidScanLicense[], componentVersion?: IComponentVersion): ILicenseReport[] { 105 | let licenseReport = [] 106 | if (componentVersion === undefined) { 107 | licenseReport = policyViolatingLicenses.map(license => createLicenseReport(license.licenseName, license._meta.href, true)) 108 | } else { 109 | const violatingPolicyLicenseNames = policyViolatingLicenses.map(license => license.licenseName) 110 | licenseReport = componentVersion.license.licenses.map(license => createLicenseReport(license.name, license.license, violatingPolicyLicenseNames.includes(license.name))) 111 | } 112 | 113 | return licenseReport 114 | } 115 | 116 | export function createComponentVulnerabilityReports(policyViolatingVulnerabilities: IRapidScanVulnerability[], componentVulnerabilities?: IComponentVulnerability[]): IVulnerabilityReport[] { 117 | let vulnerabilityReport = [] 118 | if (componentVulnerabilities === undefined) { 119 | vulnerabilityReport = policyViolatingVulnerabilities.map(vulnerability => createVulnerabilityReport(vulnerability.name, true)) 120 | } else { 121 | const violatingPolicyVulnerabilityNames = policyViolatingVulnerabilities.map(vulnerability => vulnerability.name) 122 | vulnerabilityReport = componentVulnerabilities.map(vulnerability => { 123 | const compVulnBaseScore = vulnerability.useCvss3 ? vulnerability.cvss3.baseScore : vulnerability.cvss2.baseScore 124 | return createVulnerabilityReport(vulnerability.name, violatingPolicyVulnerabilityNames.includes(vulnerability.name), vulnerability._meta.href, compVulnBaseScore, vulnerability.severity) 125 | }) 126 | } 127 | 128 | return vulnerabilityReport 129 | } 130 | 131 | export interface ILicenseReport { 132 | name: string 133 | href: string 134 | violatesPolicy: boolean 135 | } 136 | 137 | export function createLicenseReport(name: string, href: string, violatesPolicy: boolean): ILicenseReport { 138 | return { 139 | name: name, 140 | href: href, 141 | violatesPolicy: violatesPolicy 142 | } 143 | } 144 | 145 | export interface IVulnerabilityReport { 146 | name: string 147 | violatesPolicy: boolean 148 | href?: string 149 | cvssScore?: number 150 | severity?: string 151 | } 152 | 153 | export function createVulnerabilityReport(name: string, violatesPolicy: boolean, href?: string, cvssScore?: number, severity?: string): IVulnerabilityReport { 154 | return { 155 | name: name, 156 | violatesPolicy: violatesPolicy, 157 | href: href, 158 | cvssScore: cvssScore, 159 | severity: severity 160 | } 161 | } 162 | 163 | export interface IUpgradeReport { 164 | name: string 165 | href: string 166 | vulnerabilityCount: number 167 | } 168 | 169 | export function createUpgradeReport(recommendedVersion?: IRecommendedVersion): IUpgradeReport | undefined { 170 | if (recommendedVersion === undefined) { 171 | return undefined 172 | } 173 | 174 | return { 175 | name: recommendedVersion.versionName, 176 | href: recommendedVersion.version, 177 | vulnerabilityCount: Object.values(recommendedVersion.vulnerabilityRisk).reduce((accumulatedValues, value) => accumulatedValues + value, 0) 178 | } 179 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/polaris/service/PolarisAPI.ts: -------------------------------------------------------------------------------- 1 | import PolarisService from "./PolarisService"; 2 | import { 3 | IPolarisBranch, 4 | IPolarisBranchData, 5 | IPolarisCodeAnalysisEvents, IPolarisCodeAnalysisEventsData, 6 | IPolarisIssueData, 7 | IPolarisIssueDataReturn, IPolarisIssueTriage, IPolarisIssueTriageData, IPolarisIssueTriageValue, 8 | IPolarisIssueUnified, 9 | IPolarisIssueUnifiedEvent, 10 | IPolarisRun, 11 | IPolarisRunData 12 | } from "../model/PolarisAPI"; 13 | import {logger} from "../../SIGLogger"; 14 | import {DiffMap} from "../../diffmap"; 15 | import {COVERITY_NOT_PRESENT} from "../../coverity/coverity-utils"; 16 | 17 | export async function polarisGetRuns(polarisService: PolarisService, projectId: string, branchId: string): Promise { 18 | let complete = false 19 | let offset = 0 20 | let limit = 25 21 | 22 | let collected_runs = Array() 23 | 24 | while (!complete) { 25 | let run_page = await polarisGetRunsPage(polarisService, projectId, branchId, limit, offset) 26 | collected_runs = collected_runs.concat(run_page.data) 27 | offset = offset + limit 28 | if (offset >= run_page.meta.total) { 29 | complete = true 30 | } 31 | } 32 | 33 | return(collected_runs) 34 | } 35 | 36 | export async function polarisGetRunsPage(polarisService: PolarisService, projectId: string, branchId: string, 37 | limit: number, offset: number): Promise { 38 | let runs_path = `${polarisService.polaris_url}` + 39 | `/api/common/v0/runs?page[limit]=${limit}` + 40 | `&page[offset]=${offset}` + 41 | `&filter[run][project][id][eq]=${projectId}` + 42 | `&filter[run][revision][branch][id][eq]=${branchId}` 43 | 44 | logger.debug(`Fetch runs from: ${runs_path}`) 45 | 46 | const run_data = await polarisService.get_url(runs_path) 47 | 48 | //logger.debug(`Polaris runs data for projectId ${projectId} and branchId ${branchId}: ${JSON.stringify(run_data.data, null, 2)}`) 49 | 50 | const runs = run_data.data as IPolarisRunData 51 | 52 | return(runs) 53 | } 54 | 55 | export async function polarisGetBranches(polarisService: PolarisService, projectId: string): Promise { 56 | let complete = false 57 | let offset = 0 58 | let limit = 25 59 | 60 | let collected_branches = Array() 61 | 62 | while (!complete) { 63 | let branch_page = await polarisGetBranchesPage(polarisService, projectId, limit, offset) 64 | collected_branches = collected_branches.concat(branch_page.data) 65 | offset = offset + limit 66 | if (offset >= branch_page.meta.total) { 67 | complete = true 68 | } 69 | } 70 | 71 | return(collected_branches) 72 | } 73 | export async function polarisGetBranchesPage(polarisService: PolarisService, projectId: string, 74 | limit: number, offset: number): Promise { 75 | let branches_path = `${polarisService.polaris_url}` + 76 | `/api/common/v0/branches?` + 77 | `page%5Blimit%5D=${limit}` + 78 | `&page%5Boffset%5D=${offset}` + 79 | `&filter%5Bbranch%5D%5Bproject%5D%5Bid%5D%5B%24eq%5D=${projectId}` 80 | 81 | logger.debug(`Fetch branches from: ${branches_path}`) 82 | 83 | const branch_data = await polarisService.get_url(branches_path) 84 | 85 | logger.debug(`Polaris branch data for projectId ${projectId} : ${JSON.stringify(branch_data.data, null, 2)}`) 86 | 87 | const branches = branch_data.data as IPolarisBranchData 88 | 89 | return(branches) 90 | } 91 | 92 | export async function polarisGetIssuesUnified(polarisService: PolarisService, projectId: string, 93 | branchId: string, useBranch: boolean, 94 | runId: string, useRun: boolean, 95 | compareBranchId: string, 96 | compareRunId: string, 97 | filterOpenOrClosed: string): Promise { 98 | let issues = await polarisGetIssues(polarisService, projectId, 99 | useBranch ? branchId : "", 100 | useRun ? runId : "", 101 | compareBranchId, compareRunId, filterOpenOrClosed) 102 | 103 | logger.debug(`There are ${issues.issueData.length} issues for project: ${projectId} and run: ${runId}`) 104 | 105 | let issuesUnified : Array = new Array() 106 | 107 | for (const issue of issues.issueData) { 108 | logger.debug(`Issue ${issue.id} has issue key: ${issue.relationships["issue-type"]?.data.id}`) 109 | 110 | let issueEvents = undefined 111 | try { 112 | issueEvents = await polarisGetIssueEventsWithSource(polarisService, issue.attributes["finding-key"], runId) 113 | } catch(error) { 114 | logger.warn(`Unable to fetch issue events for finding key: ${issue.attributes["finding-key"]} for run: ${runId}`) 115 | } 116 | 117 | const issueTriage = await polarisGetIssueTriage(polarisService, projectId, issue.attributes["issue-key"]) 118 | 119 | let issueUnified = {} 120 | 121 | let issue_type_id = issue.relationships["issue-type"]?.data.id 122 | let issue_path_id = issue.relationships.path?.data.id 123 | let tool_id = issue.relationships.tool?.data.id 124 | let issue_opened_id = issue.relationships.transitions?.data[0].id 125 | let issue_severity_id = issue.relationships.severity?.data.id 126 | 127 | issueUnified.key = issue.attributes["issue-key"] 128 | issueUnified.checkerName = issue.attributes["sub-tool"] 129 | 130 | issueUnified.dismissed = false 131 | if (issueTriage) { 132 | let dismissStatus = await polarisGetTriageValue("DISMISS", issueTriage.attributes["triage-current-values"]) 133 | if (dismissStatus && dismissStatus["display-value"] == "Dismissed") { 134 | issueUnified.dismissed = true 135 | } 136 | } 137 | 138 | issueUnified.path = "N/A" 139 | issueUnified.name = "N/A" 140 | issueUnified.description = "N/A" 141 | issueUnified.localEffect = "N/A" 142 | issueUnified.link = "N/A" 143 | issueUnified.severity = "N/A" 144 | 145 | for (const included_data of issues.issueIncluded) { 146 | if (issue_path_id && included_data.type == "path" && included_data.id == issue_path_id) { 147 | issueUnified.path = included_data.attributes.path ? included_data.attributes.path.join('/') : "N/A" 148 | } 149 | if (issue_type_id && included_data.type == "issue-type" && included_data.id == issue_type_id) { 150 | issueUnified.name = included_data.attributes["name"] ? included_data.attributes["name"] : "N/A" 151 | issueUnified.description = included_data.attributes["description"] ? included_data.attributes["description"] : "N/A" 152 | issueUnified.localEffect = included_data.attributes["local-effect"] ? included_data.attributes["local-effect"] : "N/A" 153 | } 154 | if (issue_opened_id && included_data.type == "transition" && included_data.id == issue_opened_id) { 155 | issueUnified.link = `${polarisService.polaris_url}/projects/${projectId}/branches/${branchId}/revisions/` + 156 | `${included_data.attributes["revision-id"]}/issues/${issue.attributes["issue-key"]}` 157 | } 158 | if (issue_severity_id && included_data.type == "taxon" && included_data.id == issue_severity_id) { 159 | issueUnified.severity = included_data.attributes.name ? included_data.attributes.name : "N/A" 160 | } 161 | } 162 | 163 | issueUnified.cwe = "N/A" 164 | if (issue.relationships["related-taxa"]) { 165 | issueUnified.cwe = "" 166 | for (const taxaData of issue.relationships["related-taxa"].data) { 167 | if (issueUnified.cwe == "") { 168 | issueUnified.cwe = taxaData.id 169 | } else { 170 | issueUnified.cwe += `, ${taxaData.id}` 171 | } 172 | } 173 | } 174 | 175 | issueUnified.mainEvent = "N/A" 176 | issueUnified.mainEventDescription = "N/A" 177 | issueUnified.remediationEvent = "N/A" 178 | issueUnified.remediationEventDescription = "N/A" 179 | issueUnified.line = 1 180 | 181 | issueUnified.events = new Array() 182 | if (issueEvents) { 183 | issueUnified.line = issueEvents[0]["main-event-line-number"] 184 | for (const event of issueEvents[0].events) { 185 | if (event["event-type"] == "MAIN") { 186 | issueUnified.mainEvent = event["event-tag"] 187 | issueUnified.mainEventDescription = event["event-description"] 188 | } 189 | if (event["event-tag"] == "remediation") { 190 | issueUnified.remediationEvent = event["event-tag"] 191 | issueUnified.remediationEventDescription = event["event-description"] 192 | } 193 | 194 | let issueUnifiedEvent: IPolarisIssueUnifiedEvent = {} 195 | issueUnifiedEvent.number = event["event-number"] 196 | issueUnifiedEvent.tag = event["event-tag"] 197 | issueUnifiedEvent.type = event["event-type"] 198 | issueUnifiedEvent.description = event["event-description"] 199 | issueUnifiedEvent["line-number"] = event["line-number"] 200 | issueUnifiedEvent.filePath = event["filePath"] 201 | issueUnifiedEvent["source-after"] = event["source-after"] 202 | issueUnifiedEvent["source-before"] = event["source-before"] 203 | 204 | issueUnified.events.push(issueUnifiedEvent) 205 | } 206 | } 207 | 208 | issuesUnified.push(issueUnified) 209 | } 210 | 211 | return(issuesUnified) 212 | } 213 | 214 | export async function polarisGetIssues(polarisService: PolarisService, projectId: string, branchId: string, runId: string, 215 | compareBranchId: string, compareRunId: string, filterOpenOrClosed: string): Promise { 216 | let complete = false 217 | let offset = 0 218 | let limit = 25 219 | 220 | let collected_issues = Array() 221 | let collected_includes = Array() 222 | 223 | while (!complete) { 224 | let issues_page = await getIssuesPage(polarisService, projectId, branchId, runId, 225 | compareBranchId, compareRunId, filterOpenOrClosed, 226 | limit, offset) 227 | collected_issues = collected_issues.concat(issues_page.data) 228 | collected_includes = collected_includes.concat(issues_page.included) 229 | offset = offset + limit 230 | if (offset >= issues_page.meta.total) { 231 | complete = true 232 | } 233 | } 234 | 235 | let issueReturn = {} 236 | issueReturn.issueData = collected_issues 237 | issueReturn.issueIncluded = collected_includes 238 | return(issueReturn) 239 | } 240 | 241 | export async function getIssuesPage(polarisService: PolarisService, projectId: string, branchId: string, runId: string, 242 | compareBranchId: string, compareRunId: string, 243 | filterOpenOrClosed: string, 244 | limit: number, offset: number): Promise { 245 | let issues_path = `${polarisService.polaris_url}` + 246 | `/api/query/v1/issues?page[limit]=${limit}` + 247 | `&page[offset]=${offset}` + 248 | `&project-id=${projectId}` + 249 | `&include[issue][]=severity` + 250 | `&include[issue][]=related-taxa` 251 | 252 | if (branchId.length > 0) { 253 | issues_path += `&branch-id=${branchId}` 254 | } 255 | 256 | if (runId.length > 0) { 257 | issues_path += `&run-id=${runId}` 258 | } 259 | 260 | if (compareRunId && compareRunId.length > 0) { 261 | issues_path += `&compare-run-id=${compareRunId}` 262 | } 263 | 264 | if (compareBranchId && compareBranchId.length > 0) { 265 | issues_path += `&compare-branch-id=${compareBranchId}` 266 | } 267 | 268 | if (filterOpenOrClosed && filterOpenOrClosed.length > 0) { 269 | //issues_path += `&filter[issue][status][$eq]=${filterOpenOrClosed}` 270 | issues_path += `&filter%5Bissue%5D%5Bstatus%5D%5B%24eq%5D=${filterOpenOrClosed}` 271 | 272 | } 273 | 274 | // // curl -X GET "https://sipse.polaris.synopsys.com/api/query/v1/issues?p 275 | // roject-id=f435f59c-5abb-4957-a725-28d93f0e645b 276 | // &branch-id=c7b567ee-39ae-4ca2-8d56-7496d29f32d8 277 | // &compare-branch-id=94f11f15-2892-4496-9245-b53b6d25ca10 278 | // &filter%5Bissue%5D%5Bstatus%5D%5B%24eq%5D=closed 279 | // &page%5Blimit%5D=50" -H "accept: application/vnd.api+json" 280 | 281 | logger.debug(`Fetch issues from: ${issues_path}`) 282 | 283 | const issues_data = await polarisService.get_url(issues_path) 284 | 285 | //logger.debug(`Polaris runs data for projectId ${projectId} and branchId ${branchId} ${JSON.stringify(issues_data.data, null, 2)}`) 286 | 287 | const issues = issues_data.data as IPolarisIssueData 288 | 289 | return(issues) 290 | } 291 | 292 | export async function polarisGetIssueTriage(polarisService: PolarisService, projectId: string, issueKey: string): Promise { 293 | let triage_path = `${polarisService.polaris_url}` + 294 | `/api/triage-query/v1/triage-current/project-id%3A${projectId}` + 295 | `%3Aissue-key%3A${issueKey}` 296 | 297 | logger.debug(`Fetch issue triage from: ${triage_path}`) 298 | 299 | const triage_data = await polarisService.get_url(triage_path) 300 | 301 | //logger.debug(`Polaris triage data for projectId ${projectId} and issueKey ${issueKey} ${JSON.stringify(triage_data.data, null, 2)}`) 302 | 303 | const triage = triage_data.data as IPolarisIssueTriageData 304 | 305 | return(triage.data) 306 | } 307 | 308 | export async function polarisGetIssueEvents(polarisService: PolarisService, 309 | findingKey: string, runId: string): Promise { 310 | let events_path = `${polarisService.polaris_url}` + 311 | `/api/code-analysis/v0/events?finding-key=${findingKey}` + 312 | `&run-id=${runId}` 313 | 314 | logger.debug(`Fetch issue events from: ${events_path}`) 315 | 316 | const events_data = await polarisService.get_url(events_path) 317 | 318 | logger.debug(`Polaris events data for findingKey ${findingKey} and runId ${runId}: ${JSON.stringify(events_data.data, null, 2)}`) 319 | 320 | const events = events_data.data as IPolarisCodeAnalysisEventsData 321 | 322 | return(events.data) 323 | } 324 | 325 | export async function polarisGetIssueEventsWithSource(polarisService: PolarisService, 326 | findingKey: string, runId: string): Promise { 327 | let events_with_source_path = `${polarisService.polaris_url}` + 328 | `/api/code-analysis/v0/events-with-source?finding-key=${findingKey}` + 329 | `&run-id=${runId}` + 330 | `&occurrence-number=1` + 331 | `&max-depth=10` 332 | 333 | logger.debug(`Fetch issue events with source from: ${events_with_source_path}`) 334 | 335 | const events_with_source_data = await polarisService.get_url(events_with_source_path) 336 | 337 | logger.debug(`Polaris events with source data for findingKey ${findingKey} and runId ${runId}: ${JSON.stringify(events_with_source_data.data, null, 2)}`) 338 | 339 | const events = events_with_source_data.data as IPolarisCodeAnalysisEventsData 340 | 341 | return(events.data) 342 | } 343 | 344 | export const POLARIS_PRESENT = 'PRESENT' 345 | export const POLARIS_NOT_PRESENT = 'NOT_PRESENT' 346 | export const POLARIS_UNKNOWN_FILE = 'Unknown File' 347 | export const POLARIS_COMMENT_PREFACE = ' 354 | 355 | # Polaris Issue - ${issue.name} 356 | ${issue.mainEventDescription} ${issue.localEffect} 357 | 358 | _${issue.severity} Impact, CWE ${issue.cwe} ${issue.checkerName}_ 359 | 360 | ${issue.remediationEventDescription} 361 | 362 | [View the issue in Polaris](${issue.link}) 363 | ` 364 | } 365 | 366 | export function polarisCreateIssueCommentMessage(issue: IPolarisIssueUnified): string { 367 | const message = polarisCreateReviewCommentMessage(issue) 368 | 369 | return `${message} 370 | ## Issue location 371 | This issue was discovered outside the diff for this Pull Request. You can find it in Polaris. 372 | ` 373 | } 374 | 375 | export function polarisIsInDiff(issue: IPolarisIssueUnified, diffMap: DiffMap): boolean { 376 | const diffHunks = diffMap.get(issue.path) 377 | 378 | if (!diffHunks) { 379 | return false 380 | } 381 | 382 | return diffHunks.filter(hunk => hunk.firstLine <= issue.line).some(hunk => issue.line <= hunk.lastLine) 383 | } 384 | 385 | export async function polarisGetTriageValue(attribute_name: string, triage_values: IPolarisIssueTriageValue[]): 386 | Promise { 387 | for (const value of triage_values) { 388 | if (attribute_name == value["attribute-semantic-id"]) { 389 | return value 390 | } 391 | } 392 | return Promise.reject() 393 | } 394 | 395 | export function polarisCreateNoLongerPresentMessage(existingMessage: string, asOf: string): string { 396 | const existingMessageLines = existingMessage.split('\n') 397 | return `${existingMessageLines[0]} 398 | ${existingMessageLines[1]} 399 | ${POLARIS_NOT_PRESENT} 400 | --> 401 | 402 | Polaris issue no longer present as of: ${asOf} 403 |
404 | Show issue 405 | 406 | ${existingMessageLines.slice(4).join('\n')} 407 |
` 408 | } --------------------------------------------------------------------------------