From 9ce8a9a4f3d0506be0af341be3830c07a2b09f5d Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Sun, 15 Mar 2026 16:50:47 +0200 Subject: [PATCH 01/17] Add self-hosted validation workflow for release testing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/self-hosted-validation.yml diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml new file mode 100644 index 00000000..0242ab92 --- /dev/null +++ b/.github/workflows/self-hosted-validation.yml @@ -0,0 +1,28 @@ +name: Microsoft Security DevOps self-hosted +on: push + +permissions: + id-token: write + security-events: write + +jobs: + sample: + name: Microsoft Security DevOps + + runs-on: self-hosted + + steps: + + # Checkout your code repository to scan + - uses: actions/checkout@v6 + + # Run open source static analysis tools + - name: Run MSDO + uses: microsoft/security-devops-action@v1 + id: msdo + + # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} From 13f73717318af93f50d5ec09176ecd0817426e5b Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 10:45:44 +0200 Subject: [PATCH 02/17] feat: implement Defender CLI v2 with v1/v2 folder structure - Add v2 Defender CLI implementation (filesystem, image, model scans) - Restructure src/ and lib/ into v1/ and v2/ folders - Port defender-client and defender-installer from AzDevOps task-lib - Add job summary with SARIF parsing for GitHub Actions - Add self-hosted validation workflow for image scan testing - Add 70 new tests for v2 components Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 32 + .github/workflows/self-hosted-validation.yml | 24 +- .gitignore | 3 + action.yml | 48 +- lib/{ => v1}/container-mapping.js | 0 lib/{ => v1}/main.js | 0 lib/{ => v1}/msdo-helpers.js | 0 lib/{ => v1}/msdo-interface.js | 0 lib/{ => v1}/msdo.js | 0 lib/{ => v1}/post.js | 0 lib/{ => v1}/pre.js | 0 lib/v2/container-mapping.js | 268 +++++++++ lib/v2/defender-cli.js | 203 +++++++ lib/v2/defender-client.js | 121 ++++ lib/v2/defender-helpers.js | 174 ++++++ lib/v2/defender-installer.js | 184 ++++++ lib/v2/defender-interface.js | 7 + lib/v2/defender-main.js | 59 ++ lib/v2/job-summary.js | 277 +++++++++ lib/v2/post.js | 45 ++ lib/v2/pre.js | 45 ++ src/{ => v1}/container-mapping.ts | 582 +++++++++---------- src/{ => v1}/main.ts | 66 +-- src/{ => v1}/msdo-helpers.ts | 190 +++--- src/{ => v1}/msdo-interface.ts | 56 +- src/{ => v1}/msdo.ts | 214 +++---- src/{ => v1}/post.ts | 20 +- src/{ => v1}/pre.ts | 20 +- src/v2/container-mapping.ts | 292 ++++++++++ src/v2/defender-cli.ts | 229 ++++++++ src/v2/defender-client.ts | 143 +++++ src/v2/defender-helpers.ts | 215 +++++++ src/v2/defender-installer.ts | 193 ++++++ src/v2/defender-interface.ts | 26 + src/v2/defender-main.ts | 34 ++ src/v2/job-summary.ts | 393 +++++++++++++ src/v2/post.ts | 11 + src/v2/pre.ts | 11 + test/defender-client.tests.ts | 79 +++ test/defender-helpers.tests.ts | 180 ++++++ test/defender-installer.tests.ts | 76 +++ test/job-summary.tests.ts | 230 ++++++++ test/post.tests.ts | 2 +- test/pre.tests.ts | 2 +- 44 files changed, 4149 insertions(+), 605 deletions(-) create mode 100644 .github/copilot-instructions.md rename lib/{ => v1}/container-mapping.js (100%) rename lib/{ => v1}/main.js (100%) rename lib/{ => v1}/msdo-helpers.js (100%) rename lib/{ => v1}/msdo-interface.js (100%) rename lib/{ => v1}/msdo.js (100%) rename lib/{ => v1}/post.js (100%) rename lib/{ => v1}/pre.js (100%) create mode 100644 lib/v2/container-mapping.js create mode 100644 lib/v2/defender-cli.js create mode 100644 lib/v2/defender-client.js create mode 100644 lib/v2/defender-helpers.js create mode 100644 lib/v2/defender-installer.js create mode 100644 lib/v2/defender-interface.js create mode 100644 lib/v2/defender-main.js create mode 100644 lib/v2/job-summary.js create mode 100644 lib/v2/post.js create mode 100644 lib/v2/pre.js rename src/{ => v1}/container-mapping.ts (97%) rename src/{ => v1}/main.ts (96%) rename src/{ => v1}/msdo-helpers.ts (96%) rename src/{ => v1}/msdo-interface.ts (97%) rename src/{ => v1}/msdo.ts (97%) rename src/{ => v1}/post.ts (96%) rename src/{ => v1}/pre.ts (96%) create mode 100644 src/v2/container-mapping.ts create mode 100644 src/v2/defender-cli.ts create mode 100644 src/v2/defender-client.ts create mode 100644 src/v2/defender-helpers.ts create mode 100644 src/v2/defender-installer.ts create mode 100644 src/v2/defender-interface.ts create mode 100644 src/v2/defender-main.ts create mode 100644 src/v2/job-summary.ts create mode 100644 src/v2/post.ts create mode 100644 src/v2/pre.ts create mode 100644 test/defender-client.tests.ts create mode 100644 test/defender-helpers.tests.ts create mode 100644 test/defender-installer.tests.ts create mode 100644 test/job-summary.tests.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..ac64f605 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +# Copilot Instructions + +## Build & Test + +```bash +npm run build # Gulp: clean → sideload → compile (src/ → lib/) +npm run buildAndTest # Build + run tests +npm run buildTests # Build including test compilation +npm test # Run tests only (mocha **/*.tests.js) +npx mocha test/pre.tests.js # Run a single test file +``` + +The `@microsoft/security-devops-actions-toolkit` package comes from GitHub Packages (configured in `.npmrc`). You need a GitHub token with `read:packages` scope to `npm install`. + +## Architecture + +This is a **GitHub Action** (node20) with a three-phase lifecycle defined in `action.yml`: + +- **pre** (`pre.ts`) → runs `ContainerMapping.runPreJob()` — saves job start timestamp +- **main** (`main.ts`) → runs `MicrosoftSecurityDevOps.runMain()` — invokes the MSDO CLI with user-configured tools/categories/languages +- **post** (`post.ts`) → runs `ContainerMapping.runPostJob()` — collects Docker events/images since pre-job and reports to Defender for DevOps + +Both `MicrosoftSecurityDevOps` and `ContainerMapping` implement the `IMicrosoftSecurityDevOps` interface. The factory function `getExecutor()` in `msdo-interface.ts` instantiates them. The `container-mapping` tool is special: it runs only in pre/post phases, not through the MSDO CLI. When it's the only tool specified, `main.ts` skips execution entirely. + +The heavy lifting (CLI installation, execution, SARIF processing) lives in the `@microsoft/security-devops-actions-toolkit` package — this repo is the GitHub Action wrapper. + +## Conventions + +- **`lib/` is committed** — the official build workflow compiles TypeScript and commits the JS output to the branch. Don't add `lib/` to `.gitignore`. +- **Test files use `.tests.ts`** suffix (not `.test.ts`). Tests live in `test/` with a separate `tsconfig.json`. Compiled test JS is gitignored. +- **Testing stack**: Mocha + Sinon. Tests stub `@actions/core`, `@actions/exec`, and `https` to avoid real GitHub Action or network calls. +- **Sideloading**: Set `SECURITY_DEVOPS_ACTION_BUILD_SIDELOAD=true` to build and link a local clone of `security-devops-actions-toolkit` (expected as a sibling directory). This is handled in `gulpfile.js`. diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 0242ab92..05a0789a 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -1,4 +1,4 @@ -name: Microsoft Security DevOps self-hosted +name: Microsoft Defender CLI v2 self-hosted validation on: push permissions: @@ -6,8 +6,8 @@ permissions: security-events: write jobs: - sample: - name: Microsoft Security DevOps + defender-image-scan: + name: Defender CLI v2 - Image Scan runs-on: self-hosted @@ -16,13 +16,21 @@ jobs: # Checkout your code repository to scan - uses: actions/checkout@v6 - # Run open source static analysis tools - - name: Run MSDO - uses: microsoft/security-devops-action@v1 - id: msdo + # Run Defender CLI v2 image scan + - name: Run Defender CLI - Image Scan + uses: ./ + id: defender + with: + command: 'image' + imageName: 'ubuntu:latest' + policy: 'github' + break: 'false' + debug: 'true' + pr-summary: 'true' # Upload results to the Security tab - name: Upload results to Security tab uses: github/codeql-action/upload-sarif@v3 + if: always() with: - sarif_file: ${{ steps.msdo.outputs.sarifFile }} + sarif_file: ${{ steps.defender.outputs.sarifFile }} diff --git a/.gitignore b/.gitignore index fc6e625c..de81b306 100644 --- a/.gitignore +++ b/.gitignore @@ -329,3 +329,6 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + +# GitHub Actions Runner +actions-runner/ diff --git a/action.yml b/action.yml index 88e603b5..0771bcd2 100644 --- a/action.yml +++ b/action.yml @@ -1,35 +1,41 @@ name: 'security-devops-action' -description: 'Run security analyzers.' +description: 'Run Microsoft Defender for DevOps security scans.' author: 'Microsoft' branding: icon: 'shield' color: 'black' inputs: command: - description: Deprecated, do not use. - config: - description: A file path to a .gdnconfig file. + description: 'The scan type to perform. Options: fs (filesystem), image (container image), model (AI model).' + default: 'fs' + fileSystemPath: + description: 'The filesystem path to scan. Used when command is fs.' + default: ${{ github.workspace }} + imageName: + description: 'The container image name to scan. Used when command is image. Example: nginx:latest' + modelPath: + description: 'The AI model path or URL to scan. Used when command is model. Supports local paths and http:// or https:// URLs.' policy: - description: The name of the well known policy to use. Defaults to GitHub. - default: GitHub - categories: - description: A comma separated list of analyzer categories to run. Values secrets, code, artifacts, IaC, containers. Example IaC,secrets. Defaults to all. - languages: - description: A comma separated list of languages to analyze. Example javascript, typescript. Defaults to all. - tools: - description: A comma separated list of analyzer to run. Example bandit, binskim, container-mapping, eslint, templateanalyzer, terrascan, trivy. - includeTools: - description: Deprecated - break-on-detections: - description: If true, the action will fail the build when vulnerabilities are detected at or above the configured severity. Requires toolkit support for MSDO_BREAK. + description: 'The name of the well known policy to use. Options: github, microsoft, none.' + default: 'github' + break: + description: 'If true, the action will fail the build when critical vulnerabilities are detected.' + default: 'false' + debug: + description: 'Enable debug logging for verbose output.' default: 'false' - existingFilename: - description: A SARIF filename that already exists. If it does, then the normal run will not take place and the file will instead be uploaded to MSDO backend. + pr-summary: + description: 'Post a vulnerability summary to the GitHub Job Summary.' + default: 'true' + args: + description: 'Additional arguments to pass to the Defender CLI.' + tools: + description: 'A comma separated list of tools. Used for container-mapping backward compatibility.' outputs: sarifFile: description: A file path to a SARIF results file. runs: using: 'node20' - main: 'lib/main.js' - pre: 'lib/pre.js' - post: 'lib/post.js' + main: 'lib/v2/defender-main.js' + pre: 'lib/v2/pre.js' + post: 'lib/v2/post.js' diff --git a/lib/container-mapping.js b/lib/v1/container-mapping.js similarity index 100% rename from lib/container-mapping.js rename to lib/v1/container-mapping.js diff --git a/lib/main.js b/lib/v1/main.js similarity index 100% rename from lib/main.js rename to lib/v1/main.js diff --git a/lib/msdo-helpers.js b/lib/v1/msdo-helpers.js similarity index 100% rename from lib/msdo-helpers.js rename to lib/v1/msdo-helpers.js diff --git a/lib/msdo-interface.js b/lib/v1/msdo-interface.js similarity index 100% rename from lib/msdo-interface.js rename to lib/v1/msdo-interface.js diff --git a/lib/msdo.js b/lib/v1/msdo.js similarity index 100% rename from lib/msdo.js rename to lib/v1/msdo.js diff --git a/lib/post.js b/lib/v1/post.js similarity index 100% rename from lib/post.js rename to lib/v1/post.js diff --git a/lib/pre.js b/lib/v1/pre.js similarity index 100% rename from lib/pre.js rename to lib/v1/pre.js diff --git a/lib/v2/container-mapping.js b/lib/v2/container-mapping.js new file mode 100644 index 00000000..f0908b59 --- /dev/null +++ b/lib/v2/container-mapping.js @@ -0,0 +1,268 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ContainerMapping = void 0; +const https = __importStar(require("https")); +const core = __importStar(require("@actions/core")); +const exec = __importStar(require("@actions/exec")); +const os = __importStar(require("os")); +const sendReportRetryCount = 1; +const GetScanContextURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; +const ContainerMappingURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; +class ContainerMapping { + constructor() { + this.succeedOnError = true; + } + runPreJob() { + try { + core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + this._runPreJob(); + } + catch (error) { + core.info("Error in Container Mapping pre-job: " + error); + } + finally { + core.info("::endgroup::"); + } + } + _runPreJob() { + const startTime = new Date().toISOString(); + core.saveState('PreJobStartTime', startTime); + core.info(`PreJobStartTime: ${startTime}`); + } + runMain() { + return __awaiter(this, void 0, void 0, function* () { + }); + } + runPostJob() { + return __awaiter(this, void 0, void 0, function* () { + try { + core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + yield this._runPostJob(); + } + catch (error) { + core.info("Error in Container Mapping post-job: " + error); + } + finally { + core.info("::endgroup::"); + } + }); + } + _runPostJob() { + return __awaiter(this, void 0, void 0, function* () { + let startTime = core.getState('PreJobStartTime'); + if (startTime.length <= 0) { + startTime = new Date(new Date().getTime() - 10000).toISOString(); + core.debug(`PreJobStartTime not defined, using now-10secs`); + } + core.info(`PreJobStartTime: ${startTime}`); + let reportData = { + dockerVersion: "", + dockerEvents: [], + dockerImages: [] + }; + let bearerToken = yield core.getIDToken() + .then((token) => { return token; }) + .catch((error) => { + throw new Error("Unable to get token: " + error); + }); + if (!bearerToken) { + throw new Error("Empty OIDC token received"); + } + var callerIsOnboarded = yield this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); + if (!callerIsOnboarded) { + core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload."); + return; + } + core.info("Client is onboarded for container mapping."); + let dockerVersionOutput = yield exec.getExecOutput('docker --version'); + if (dockerVersionOutput.exitCode != 0) { + core.info(`Unable to get docker version: ${dockerVersionOutput}`); + core.info(`Skipping container mapping since docker not found/available.`); + return; + } + reportData.dockerVersion = dockerVersionOutput.stdout.trim(); + yield this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) + .catch((error) => { + throw new Error("Unable to get docker events: " + error); + }); + yield this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) + .catch((error) => { + throw new Error("Unable to get docker images: " + error); + }); + core.debug("Finished data collection, starting API calls."); + var reportSent = yield this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); + if (!reportSent) { + throw new Error("Unable to send report to backend service"); + } + ; + core.info("Container mapping data sent successfully."); + }); + } + execCommand(command, listener) { + return __awaiter(this, void 0, void 0, function* () { + return exec.getExecOutput(command) + .then((result) => { + if (result.exitCode != 0) { + return Promise.reject(`Command execution failed: ${result}`); + } + result.stdout.trim().split(os.EOL).forEach(element => { + if (element.length > 0) { + listener.push(element); + } + }); + }); + }); + } + sendReport(data, bearerToken, retryCount = 0) { + return __awaiter(this, void 0, void 0, function* () { + core.debug(`attempting to send report: ${data}`); + return yield this._sendReport(data, bearerToken) + .then(() => { + return true; + }) + .catch((error) => __awaiter(this, void 0, void 0, function* () { + if (retryCount == 0) { + return false; + } + else { + core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); + retryCount--; + return yield this.sendReport(data, bearerToken, retryCount); + } + })); + }); + } + _sendReport(data, bearerToken) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + let apiTime = new Date().getMilliseconds(); + let options = { + method: 'POST', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + 'Content-Length': data.length + } + }; + core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); + const req = https.request(ContainerMappingURL, options, (res) => { + let resData = ''; + res.on('data', (chunk) => { + resData += chunk.toString(); + }); + res.on('end', () => { + core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); + core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); + core.debug('Response headers: ' + JSON.stringify(res.headers)); + if (resData.length > 0) { + core.debug('Response: ' + resData); + } + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); + } + resolve(); + }); + }); + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + req.write(data); + req.end(); + })); + }); + } + checkCallerIsCustomer(bearerToken, retryCount = 0) { + return __awaiter(this, void 0, void 0, function* () { + return yield this._checkCallerIsCustomer(bearerToken) + .then((statusCode) => __awaiter(this, void 0, void 0, function* () { + if (statusCode == 200) { + return true; + } + else if (statusCode == 403) { + return false; + } + else { + core.debug(`Unexpected status code: ${statusCode}`); + return yield this.retryCall(bearerToken, retryCount); + } + })) + .catch((error) => __awaiter(this, void 0, void 0, function* () { + core.info(`Unexpected error: ${error}.`); + return yield this.retryCall(bearerToken, retryCount); + })); + }); + } + retryCall(bearerToken, retryCount) { + return __awaiter(this, void 0, void 0, function* () { + if (retryCount == 0) { + core.info(`All retries failed.`); + return false; + } + else { + core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); + retryCount--; + return yield this.checkCallerIsCustomer(bearerToken, retryCount); + } + }); + } + _checkCallerIsCustomer(bearerToken) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + let options = { + method: 'GET', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + } + }; + core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); + const req = https.request(GetScanContextURL, options, (res) => { + res.on('end', () => { + resolve(res.statusCode); + }); + res.on('data', function (d) { + }); + }); + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + req.end(); + })); + }); + } +} +exports.ContainerMapping = ContainerMapping; diff --git a/lib/v2/defender-cli.js b/lib/v2/defender-cli.js new file mode 100644 index 00000000..9d9c1084 --- /dev/null +++ b/lib/v2/defender-cli.js @@ -0,0 +1,203 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MicrosoftDefenderCLI = void 0; +const core = __importStar(require("@actions/core")); +const exec = __importStar(require("@actions/exec")); +const path = __importStar(require("path")); +const defender_helpers_1 = require("./defender-helpers"); +const defender_client_1 = require("./defender-client"); +const job_summary_1 = require("./job-summary"); +class MicrosoftDefenderCLI { + constructor() { + this.prSummaryEnabled = true; + this.succeedOnError = false; + } + runPreJob() { + return __awaiter(this, void 0, void 0, function* () { + }); + } + runPostJob() { + return __awaiter(this, void 0, void 0, function* () { + }); + } + runMain() { + return __awaiter(this, void 0, void 0, function* () { + yield this.runDefenderCLI(); + }); + } + runDefenderCLI() { + return __awaiter(this, void 0, void 0, function* () { + const debugInput = core.getInput(defender_helpers_1.Inputs.Debug); + const debug = debugInput ? debugInput.toLowerCase() === 'true' : false; + if (debug) { + (0, defender_helpers_1.setupDebugLogging)(true); + core.debug('Debug logging enabled'); + } + const command = core.getInput(defender_helpers_1.Inputs.Command) || 'fs'; + const scanType = (0, defender_helpers_1.validateScanType)(command); + const prSummaryInput = core.getInput(defender_helpers_1.Inputs.PrSummary); + this.prSummaryEnabled = prSummaryInput ? prSummaryInput.toLowerCase() !== 'false' : true; + core.debug(`PR Summary enabled: ${this.prSummaryEnabled}`); + const argsInput = core.getInput(defender_helpers_1.Inputs.Args) || ''; + let additionalArgs = (0, defender_helpers_1.parseAdditionalArgs)(argsInput); + let target; + switch (scanType) { + case defender_helpers_1.ScanType.FileSystem: + const fileSystemPath = core.getInput(defender_helpers_1.Inputs.FileSystemPath) || + process.env['GITHUB_WORKSPACE'] || + process.cwd(); + target = (0, defender_helpers_1.validateFileSystemPath)(fileSystemPath); + core.debug(`Filesystem scan using directory: ${target}`); + break; + case defender_helpers_1.ScanType.Image: + const imageName = core.getInput(defender_helpers_1.Inputs.ImageName); + if (!imageName) { + throw new Error('Image name is required for image scan'); + } + target = (0, defender_helpers_1.validateImageName)(imageName); + break; + case defender_helpers_1.ScanType.Model: + const modelPath = core.getInput(defender_helpers_1.Inputs.ModelPath); + if (!modelPath) { + throw new Error('Model path is required for model scan'); + } + target = (0, defender_helpers_1.validateModelPath)(modelPath); + break; + default: + throw new Error(`Unsupported scan type: ${scanType}`); + } + const breakInput = core.getInput(defender_helpers_1.Inputs.Break); + const breakOnCritical = breakInput ? breakInput.toLowerCase() === 'true' : false; + additionalArgs = additionalArgs.filter(arg => arg !== '--defender-break'); + if (breakOnCritical) { + additionalArgs.push('--defender-break'); + core.debug('Break on critical vulnerability enabled: adding --defender-break flag'); + } + additionalArgs = additionalArgs.filter(arg => arg !== '--defender-debug'); + if (debug) { + additionalArgs.push('--defender-debug'); + core.debug('Debug mode enabled: adding --defender-debug flag'); + } + let successfulExitCodes = [0]; + const outputPath = path.join(process.env['RUNNER_TEMP'] || process.cwd(), 'defender.sarif'); + const policyInput = core.getInput(defender_helpers_1.Inputs.Policy) || 'github'; + let policy; + if (policyInput === 'none') { + policy = ''; + } + else { + policy = policyInput; + } + core.debug(`Scan Type: ${scanType}`); + core.debug(`Target: ${target}`); + core.debug(`Policy: ${policy}`); + core.debug(`Output Path: ${outputPath}`); + if (additionalArgs.length > 0) { + core.debug(`Additional Arguments: ${additionalArgs.join(' ')}`); + } + process.env['Defender_Extension'] = 'true'; + core.debug('Environment variable set: Defender_Extension=true'); + try { + switch (scanType) { + case defender_helpers_1.ScanType.FileSystem: + yield (0, defender_client_1.scanDirectory)(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + case defender_helpers_1.ScanType.Image: + yield (0, defender_client_1.scanImage)(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + case defender_helpers_1.ScanType.Model: + yield this.runModelScan(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + } + if (this.prSummaryEnabled) { + core.debug('Posting job summary...'); + yield (0, job_summary_1.postJobSummary)(outputPath, scanType, target); + } + } + catch (error) { + if (this.prSummaryEnabled) { + try { + yield (0, job_summary_1.postJobSummary)(outputPath, scanType, target); + } + catch (summaryError) { + core.debug(`Failed to post summary after error: ${summaryError}`); + } + } + core.error(`Defender CLI execution failed: ${error}`); + throw error; + } + }); + } + runModelScan(modelPath, policy, outputPath, successfulExitCodes, additionalArgs) { + return __awaiter(this, void 0, void 0, function* () { + const cliFilePath = process.env['DEFENDER_FILEPATH']; + if (!cliFilePath) { + throw new Error('DEFENDER_FILEPATH environment variable is not set. Defender CLI may not be installed.'); + } + const args = [ + 'scan', + 'model', + modelPath, + ]; + if (policy) { + args.push('--defender-policy', policy); + } + args.push('--defender-output', outputPath); + if (additionalArgs && additionalArgs.length > 0) { + args.push(...additionalArgs); + core.debug(`Appending additional arguments: ${additionalArgs.join(' ')}`); + } + const isDebug = process.env['RUNNER_DEBUG'] === '1' || core.isDebug(); + if (isDebug && !args.includes('--defender-debug')) { + args.push('--defender-debug'); + } + core.debug('Running Microsoft Defender CLI for model scan...'); + const exitCode = yield exec.exec(cliFilePath, args, { + ignoreReturnCode: true + }); + let success = false; + for (const successCode of successfulExitCodes) { + if (exitCode === successCode) { + success = true; + break; + } + } + if (!success) { + throw new Error(`Defender CLI exited with an error exit code: ${exitCode}`); + } + }); + } +} +exports.MicrosoftDefenderCLI = MicrosoftDefenderCLI; diff --git a/lib/v2/defender-client.js b/lib/v2/defender-client.js new file mode 100644 index 00000000..b2d6e608 --- /dev/null +++ b/lib/v2/defender-client.js @@ -0,0 +1,121 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.scanImage = exports.scanDirectory = void 0; +const core = __importStar(require("@actions/core")); +const exec = __importStar(require("@actions/exec")); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const installer = __importStar(require("./defender-installer")); +function scanDirectory(directoryPath, policy, outputPath, successfulExitCodes, additionalArgs) { + return __awaiter(this, void 0, void 0, function* () { + yield scan('fs', directoryPath, policy, outputPath, successfulExitCodes, additionalArgs); + }); +} +exports.scanDirectory = scanDirectory; +function scanImage(imageName, policy, outputPath, successfulExitCodes, additionalArgs) { + return __awaiter(this, void 0, void 0, function* () { + yield scan('image', imageName, policy, outputPath, successfulExitCodes, additionalArgs); + }); +} +exports.scanImage = scanImage; +function scan(scanType, target, policy, outputPath, successfulExitCodes, additionalArgs) { + return __awaiter(this, void 0, void 0, function* () { + const resolvedPolicy = policy || 'mdc'; + const resolvedOutputPath = outputPath || path.join(process.env['RUNNER_TEMP'] || process.cwd(), 'defender.sarif'); + const inputArgs = [ + 'scan', + scanType, + target, + '--defender-policy', + resolvedPolicy, + '--defender-output', + resolvedOutputPath + ]; + if (additionalArgs && additionalArgs.length > 0) { + inputArgs.push(...additionalArgs); + } + yield runDefenderCli(inputArgs, successfulExitCodes); + }); +} +function runDefenderCli(inputArgs, successfulExitCodes) { + return __awaiter(this, void 0, void 0, function* () { + yield setupEnvironment(); + const cliFilePath = getCliFilePath(); + if (!cliFilePath) { + throw new Error('DEFENDER_FILEPATH environment variable is not set. Defender CLI may not be installed.'); + } + core.debug(`Running Defender CLI: ${cliFilePath} ${inputArgs.join(' ')}`); + const isDebug = process.env['RUNNER_DEBUG'] === '1' || core.isDebug(); + if (isDebug && !inputArgs.includes('--defender-debug')) { + inputArgs.push('--defender-debug'); + } + const exitCode = yield exec.exec(cliFilePath, inputArgs, { + ignoreReturnCode: true + }); + const validExitCodes = successfulExitCodes || [0]; + if (!validExitCodes.includes(exitCode)) { + throw new Error(`Defender CLI exited with an error exit code: ${exitCode}`); + } + core.debug(`Defender CLI completed successfully with exit code: ${exitCode}`); + }); +} +function setupEnvironment() { + return __awaiter(this, void 0, void 0, function* () { + const toolCacheDir = process.env['RUNNER_TOOL_CACHE'] || path.join(os.homedir(), '.defender'); + const defenderDir = path.join(toolCacheDir, '_defender'); + if (!fs.existsSync(defenderDir)) { + fs.mkdirSync(defenderDir, { recursive: true }); + } + const packagesDirectory = process.env['DEFENDER_PACKAGES_DIRECTORY'] || path.join(defenderDir, 'packages'); + process.env['DEFENDER_PACKAGES_DIRECTORY'] = packagesDirectory; + if (!process.env['DEFENDER_FILEPATH']) { + const cliVersion = resolveCliVersion(); + core.debug(`Installing Defender CLI version: ${cliVersion}`); + yield installer.install(cliVersion); + } + }); +} +function resolveCliVersion() { + let version = process.env['DEFENDER_VERSION'] || 'latest'; + if (version.includes('*')) { + version = 'Latest'; + } + core.debug(`Resolved Defender CLI version: ${version}`); + return version; +} +function getCliFilePath() { + return process.env['DEFENDER_FILEPATH']; +} diff --git a/lib/v2/defender-helpers.js b/lib/v2/defender-helpers.js new file mode 100644 index 00000000..e31b7c96 --- /dev/null +++ b/lib/v2/defender-helpers.js @@ -0,0 +1,174 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.parseAdditionalArgs = exports.getEncodedContent = exports.encode = exports.writeToOutStream = exports.setupDebugLogging = exports.validateImageName = exports.validateModelPath = exports.validateModelUrl = exports.isUrl = exports.validateFileSystemPath = exports.validateScanType = exports.Constants = exports.ScanType = exports.Inputs = void 0; +const core = __importStar(require("@actions/core")); +const fs = __importStar(require("fs")); +const os = __importStar(require("os")); +var Inputs; +(function (Inputs) { + Inputs["Command"] = "command"; + Inputs["Args"] = "args"; + Inputs["FileSystemPath"] = "fileSystemPath"; + Inputs["ImageName"] = "imageName"; + Inputs["ModelPath"] = "modelPath"; + Inputs["Break"] = "break"; + Inputs["Debug"] = "debug"; + Inputs["PrSummary"] = "pr-summary"; + Inputs["Policy"] = "policy"; +})(Inputs || (exports.Inputs = Inputs = {})); +var ScanType; +(function (ScanType) { + ScanType["FileSystem"] = "fs"; + ScanType["Image"] = "image"; + ScanType["Model"] = "model"; +})(ScanType || (exports.ScanType = ScanType = {})); +var Constants; +(function (Constants) { + Constants["Unknown"] = "unknown"; + Constants["PreJobStartTime"] = "PREJOBSTARTTIME"; + Constants["DefenderExecutable"] = "Defender"; +})(Constants || (exports.Constants = Constants = {})); +function validateScanType(scanTypeInput) { + const scanType = scanTypeInput; + if (!Object.values(ScanType).includes(scanType)) { + throw new Error(`Invalid scan type: ${scanTypeInput}. Valid options are: ${Object.values(ScanType).join(', ')}`); + } + return scanType; +} +exports.validateScanType = validateScanType; +function validateFileSystemPath(fsPath) { + if (!fsPath || fsPath.trim() === '') { + throw new Error('Filesystem path cannot be empty for filesystem scan'); + } + const trimmedPath = fsPath.trim(); + if (!fs.existsSync(trimmedPath)) { + throw new Error(`Filesystem path does not exist: ${trimmedPath}`); + } + return trimmedPath; +} +exports.validateFileSystemPath = validateFileSystemPath; +function isUrl(input) { + if (!input) { + return false; + } + const lowercased = input.toLowerCase(); + return lowercased.startsWith('http://') || lowercased.startsWith('https://'); +} +exports.isUrl = isUrl; +function validateModelUrl(url) { + try { + const parsedUrl = new URL(url); + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new Error(`Invalid URL protocol: ${parsedUrl.protocol}. Only http:// and https:// are supported.`); + } + if (!parsedUrl.hostname) { + throw new Error('URL must have a valid hostname.'); + } + return url; + } + catch (error) { + if (error instanceof TypeError) { + throw new Error(`Invalid URL format: ${url}`); + } + throw error; + } +} +exports.validateModelUrl = validateModelUrl; +function validateModelPath(modelPath) { + if (!modelPath || modelPath.trim() === '') { + throw new Error('Model path cannot be empty for model scan'); + } + const trimmedPath = modelPath.trim(); + if (isUrl(trimmedPath)) { + return validateModelUrl(trimmedPath); + } + if (!fs.existsSync(trimmedPath)) { + throw new Error(`Model path does not exist: ${trimmedPath}`); + } + const stats = fs.statSync(trimmedPath); + if (!stats.isFile() && !stats.isDirectory()) { + throw new Error(`Model path must be a file or directory: ${trimmedPath}`); + } + return trimmedPath; +} +exports.validateModelPath = validateModelPath; +function validateImageName(imageName) { + if (!imageName || imageName.trim() === '') { + throw new Error('Image name cannot be empty for image scan'); + } + const trimmedImageName = imageName.trim(); + const imageNameRegex = /^(?:(?:[a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*(?::[0-9]+)?\/)?[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)*)(?::[a-zA-Z0-9._-]+|@sha256:[a-fA-F0-9]{64})?$/; + if (!imageNameRegex.test(trimmedImageName)) { + throw new Error(`Invalid image name format: ${trimmedImageName}. Image name should follow container image naming conventions.`); + } + return trimmedImageName; +} +exports.validateImageName = validateImageName; +function setupDebugLogging(enabled) { + if (enabled) { + process.env['RUNNER_DEBUG'] = '1'; + core.debug('Debug logging enabled'); + } +} +exports.setupDebugLogging = setupDebugLogging; +function writeToOutStream(data, outStream = process.stdout) { + outStream.write(data.trim() + os.EOL); +} +exports.writeToOutStream = writeToOutStream; +const encode = (str) => Buffer.from(str, 'binary').toString('base64'); +exports.encode = encode; +function getEncodedContent(dockerVersion, dockerEvents, dockerImages) { + let data = []; + data.push('DockerVersion: ' + dockerVersion); + data.push('DockerEvents:'); + data.push(dockerEvents); + data.push('DockerImages:'); + data.push(dockerImages); + return (0, exports.encode)(data.join(os.EOL)); +} +exports.getEncodedContent = getEncodedContent; +function parseAdditionalArgs(additionalArgs) { + if (!additionalArgs || additionalArgs.trim() === '') { + return []; + } + const args = []; + const trimmedArgs = additionalArgs.trim(); + const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g; + const matches = trimmedArgs.match(regex); + if (matches) { + for (const match of matches) { + let arg = match; + if ((arg.startsWith('"') && arg.endsWith('"')) || + (arg.startsWith("'") && arg.endsWith("'"))) { + arg = arg.slice(1, -1); + } + args.push(arg); + } + } + core.debug(`Parsed additional arguments: ${JSON.stringify(args)}`); + return args; +} +exports.parseAdditionalArgs = parseAdditionalArgs; diff --git a/lib/v2/defender-installer.js b/lib/v2/defender-installer.js new file mode 100644 index 00000000..966efe6e --- /dev/null +++ b/lib/v2/defender-installer.js @@ -0,0 +1,184 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setVariables = exports.resolveFileName = exports.install = void 0; +const core = __importStar(require("@actions/core")); +const fs = __importStar(require("fs")); +const https = __importStar(require("https")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const downloadBaseUrl = 'https://cli.dfd.security.azure.com/public'; +const maxRetries = 3; +const downloadTimeoutMs = 30000; +function install(cliVersion = 'latest') { + return __awaiter(this, void 0, void 0, function* () { + const existingPath = process.env['DEFENDER_FILEPATH']; + if (existingPath && fs.existsSync(existingPath)) { + core.debug(`Defender CLI already installed at: ${existingPath}`); + return; + } + const existingDir = process.env['DEFENDER_DIRECTORY']; + if (existingDir && fs.existsSync(existingDir)) { + const fileName = resolveFileName(); + const filePath = path.join(existingDir, fileName); + if (fs.existsSync(filePath)) { + core.debug(`Found pre-installed Defender CLI at: ${filePath}`); + setVariables(existingDir, fileName, cliVersion); + return; + } + } + const toolCacheDir = process.env['RUNNER_TOOL_CACHE'] || path.join(os.homedir(), '.defender'); + const packagesDirectory = process.env['DEFENDER_PACKAGES_DIRECTORY'] || path.join(toolCacheDir, '_defender', 'packages'); + if (!fs.existsSync(packagesDirectory)) { + fs.mkdirSync(packagesDirectory, { recursive: true }); + } + const fileName = resolveFileName(); + let lastError; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + core.info(`Downloading Defender CLI (attempt ${attempt}/${maxRetries})...`); + yield downloadDefenderCli(packagesDirectory, fileName, cliVersion); + setVariables(packagesDirectory, fileName, cliVersion, true); + core.info(`Defender CLI installed successfully.`); + return; + } + catch (error) { + lastError = error; + core.warning(`Download attempt ${attempt} failed: ${lastError.message}`); + if (attempt < maxRetries) { + core.info('Retrying...'); + } + } + } + throw new Error(`Failed to install Defender CLI after ${maxRetries} attempts: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`); + }); +} +exports.install = install; +function downloadDefenderCli(packagesDirectory, fileName, cliVersion) { + return __awaiter(this, void 0, void 0, function* () { + const versionDir = path.join(packagesDirectory, `defender-cli.${cliVersion}`); + if (!fs.existsSync(versionDir)) { + fs.mkdirSync(versionDir, { recursive: true }); + } + const filePath = path.join(versionDir, fileName); + const downloadUrl = `${downloadBaseUrl}/${cliVersion.toLowerCase()}/${fileName}`; + core.debug(`Downloading from: ${downloadUrl}`); + core.debug(`Saving to: ${filePath}`); + yield downloadFile(downloadUrl, filePath); + if (process.platform !== 'win32') { + fs.chmodSync(filePath, 0o755); + } + }); +} +function downloadFile(url, filePath) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + const request = https.get(url, { timeout: downloadTimeoutMs }, (response) => { + if (response.statusCode === 301 || response.statusCode === 302) { + file.close(); + fs.unlinkSync(filePath); + const redirectUrl = response.headers.location; + if (!redirectUrl) { + return reject(new Error('Redirect without location header')); + } + core.debug(`Following redirect to: ${redirectUrl}`); + downloadFile(redirectUrl, filePath).then(resolve).catch(reject); + return; + } + if (response.statusCode !== 200) { + file.close(); + fs.unlinkSync(filePath); + return reject(new Error(`Download failed with status code: ${response.statusCode}`)); + } + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }); + request.on('error', (error) => { + file.close(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + reject(new Error(`Download error: ${error.message}`)); + }); + request.on('timeout', () => { + request.destroy(); + file.close(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + reject(new Error('Download timed out')); + }); + }); +} +function resolveFileName() { + const platform = os.platform(); + const arch = os.arch(); + switch (platform) { + case 'win32': + if (arch === 'arm64') + return 'Defender_win-arm64.exe'; + if (arch === 'ia32' || arch === 'x32') + return 'Defender_win-x86.exe'; + return 'Defender_win-x64.exe'; + case 'linux': + if (arch === 'arm64') + return 'Defender_linux-arm64'; + return 'Defender_linux-x64'; + case 'darwin': + if (arch === 'arm64') + return 'Defender_osx-arm64'; + return 'Defender_osx-x64'; + default: + core.warning(`Unknown platform: ${platform}. Defaulting to linux-x64.`); + return 'Defender_linux-x64'; + } +} +exports.resolveFileName = resolveFileName; +function setVariables(packagesDirectory, fileName, cliVersion, validate = false) { + const defenderDir = path.join(packagesDirectory, `defender-cli.${cliVersion}`); + const defenderFilePath = path.join(defenderDir, fileName); + if (validate && !fs.existsSync(defenderFilePath)) { + throw new Error(`Defender CLI not found after download: ${defenderFilePath}`); + } + process.env['DEFENDER_DIRECTORY'] = defenderDir; + process.env['DEFENDER_FILEPATH'] = defenderFilePath; + process.env['DEFENDER_INSTALLEDVERSION'] = cliVersion; + core.debug(`DEFENDER_DIRECTORY=${defenderDir}`); + core.debug(`DEFENDER_FILEPATH=${defenderFilePath}`); + core.debug(`DEFENDER_INSTALLEDVERSION=${cliVersion}`); +} +exports.setVariables = setVariables; diff --git a/lib/v2/defender-interface.js b/lib/v2/defender-interface.js new file mode 100644 index 00000000..6b0ba53d --- /dev/null +++ b/lib/v2/defender-interface.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getDefenderExecutor = void 0; +function getDefenderExecutor(runner) { + return new runner(); +} +exports.getDefenderExecutor = getDefenderExecutor; diff --git a/lib/v2/defender-main.js b/lib/v2/defender-main.js new file mode 100644 index 00000000..4d03025f --- /dev/null +++ b/lib/v2/defender-main.js @@ -0,0 +1,59 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const defender_cli_1 = require("./defender-cli"); +const defender_interface_1 = require("./defender-interface"); +const defender_helpers_1 = require("./defender-helpers"); +let succeedOnError = false; +function _getDefenderRunner() { + return (0, defender_interface_1.getDefenderExecutor)(defender_cli_1.MicrosoftDefenderCLI); +} +function run() { + return __awaiter(this, void 0, void 0, function* () { + core.debug('Starting Microsoft Defender for DevOps scan'); + const defenderRunner = _getDefenderRunner(); + succeedOnError = defenderRunner.succeedOnError; + yield defenderRunner.runMain(); + }); +} +run().catch(error => { + if (succeedOnError) { + (0, defender_helpers_1.writeToOutStream)('Ran into error: ' + error); + core.info('Finished execution with error (succeedOnError=true)'); + } + else { + core.setFailed(error); + } +}); diff --git a/lib/v2/job-summary.js b/lib/v2/job-summary.js new file mode 100644 index 00000000..63d7f32b --- /dev/null +++ b/lib/v2/job-summary.js @@ -0,0 +1,277 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.postJobSummary = exports.generateNoFindingsSummary = exports.generateMarkdownSummary = exports.parseSarifContent = exports.formatLocation = exports.extractCveId = exports.mapLevelToSeverity = exports.Severity = exports.SarifLevel = void 0; +const core = __importStar(require("@actions/core")); +const fs = __importStar(require("fs")); +var SarifLevel; +(function (SarifLevel) { + SarifLevel["Error"] = "error"; + SarifLevel["Warning"] = "warning"; + SarifLevel["Note"] = "note"; + SarifLevel["None"] = "none"; +})(SarifLevel || (exports.SarifLevel = SarifLevel = {})); +var Severity; +(function (Severity) { + Severity["Critical"] = "critical"; + Severity["High"] = "high"; + Severity["Medium"] = "medium"; + Severity["Low"] = "low"; + Severity["Unknown"] = "unknown"; +})(Severity || (exports.Severity = Severity = {})); +function mapLevelToSeverity(level, properties) { + if (properties === null || properties === void 0 ? void 0 : properties.severity) { + const propSeverity = properties.severity.toLowerCase(); + if (propSeverity === 'critical') + return Severity.Critical; + if (propSeverity === 'high') + return Severity.High; + if (propSeverity === 'medium') + return Severity.Medium; + if (propSeverity === 'low') + return Severity.Low; + } + switch (level === null || level === void 0 ? void 0 : level.toLowerCase()) { + case SarifLevel.Error: + return Severity.High; + case SarifLevel.Warning: + return Severity.Medium; + case SarifLevel.Note: + return Severity.Low; + case SarifLevel.None: + return Severity.Low; + default: + return Severity.Unknown; + } +} +exports.mapLevelToSeverity = mapLevelToSeverity; +function extractCveId(ruleId, properties) { + if (properties === null || properties === void 0 ? void 0 : properties.cveId) { + return properties.cveId; + } + if (ruleId) { + const cveMatch = ruleId.match(/CVE-\d{4}-\d+/i); + if (cveMatch) { + return cveMatch[0].toUpperCase(); + } + } + return undefined; +} +exports.extractCveId = extractCveId; +function formatLocation(locations) { + var _a, _b, _c, _d; + if (!locations || locations.length === 0) { + return undefined; + } + const loc = locations[0]; + const uri = (_b = (_a = loc.physicalLocation) === null || _a === void 0 ? void 0 : _a.artifactLocation) === null || _b === void 0 ? void 0 : _b.uri; + const line = (_d = (_c = loc.physicalLocation) === null || _c === void 0 ? void 0 : _c.region) === null || _d === void 0 ? void 0 : _d.startLine; + if (uri) { + return line ? `${uri}:${line}` : uri; + } + return undefined; +} +exports.formatLocation = formatLocation; +function parseSarifContent(sarifContent) { + var _a, _b, _c, _d, _e; + const summary = { + total: 0, + critical: 0, + high: 0, + medium: 0, + low: 0, + unknown: 0, + vulnerabilities: [] + }; + let sarif; + try { + sarif = JSON.parse(sarifContent); + } + catch (error) { + core.warning(`Failed to parse SARIF content: ${error}`); + return summary; + } + if (!sarif.runs || sarif.runs.length === 0) { + core.debug('No runs found in SARIF document'); + return summary; + } + const rulesMap = new Map(); + for (const run of sarif.runs) { + if ((_b = (_a = run.tool) === null || _a === void 0 ? void 0 : _a.driver) === null || _b === void 0 ? void 0 : _b.rules) { + for (const rule of run.tool.driver.rules) { + rulesMap.set(rule.id, rule); + } + } + if (run.results) { + for (const result of run.results) { + const ruleId = result.ruleId || 'unknown'; + const rule = rulesMap.get(ruleId); + const severity = mapLevelToSeverity(result.level || ((_c = rule === null || rule === void 0 ? void 0 : rule.defaultConfiguration) === null || _c === void 0 ? void 0 : _c.level), result.properties || (rule === null || rule === void 0 ? void 0 : rule.properties)); + const vulnerability = { + ruleId, + message: ((_d = result.message) === null || _d === void 0 ? void 0 : _d.text) || ((_e = rule === null || rule === void 0 ? void 0 : rule.shortDescription) === null || _e === void 0 ? void 0 : _e.text) || 'No description available', + severity, + location: formatLocation(result.locations), + cveId: extractCveId(ruleId, result.properties) + }; + summary.vulnerabilities.push(vulnerability); + summary.total++; + switch (severity) { + case Severity.Critical: + summary.critical++; + break; + case Severity.High: + summary.high++; + break; + case Severity.Medium: + summary.medium++; + break; + case Severity.Low: + summary.low++; + break; + default: + summary.unknown++; + } + } + } + } + return summary; +} +exports.parseSarifContent = parseSarifContent; +function generateMarkdownSummary(summary, scanType, target, hasCriticalOrHigh) { + const lines = []; + lines.push('# Microsoft Defender for DevOps Scan Results'); + lines.push(''); + lines.push('## Summary'); + lines.push('| Severity | Count |'); + lines.push('|----------|-------|'); + lines.push(`| 🔴 Critical | ${summary.critical} |`); + lines.push(`| 🟠 High | ${summary.high} |`); + lines.push(`| 🟡 Medium | ${summary.medium} |`); + lines.push(`| 🟢 Low | ${summary.low} |`); + if (summary.unknown > 0) { + lines.push(`| ⚪ Unknown | ${summary.unknown} |`); + } + lines.push(''); + lines.push(`**Total Vulnerabilities**: ${summary.total}`); + lines.push(''); + if (summary.critical > 0 || summary.high > 0) { + lines.push('## Critical and High Findings'); + const criticalAndHigh = summary.vulnerabilities.filter(v => v.severity === Severity.Critical || v.severity === Severity.High); + let index = 1; + for (const vuln of criticalAndHigh.slice(0, 20)) { + const severityIcon = vuln.severity === Severity.Critical ? '🔴' : '🟠'; + const identifier = vuln.cveId || vuln.ruleId; + const location = vuln.location ? ` in \`${vuln.location}\`` : ''; + lines.push(`${index}. ${severityIcon} **${identifier}** - ${vuln.message}${location}`); + index++; + } + if (criticalAndHigh.length > 20) { + lines.push(`... and ${criticalAndHigh.length - 20} more`); + } + lines.push(''); + } + lines.push('## Scan Details'); + lines.push(`- **Scan Type**: ${formatScanType(scanType)}`); + lines.push(`- **Target**: \`${target}\``); + const statusIcon = hasCriticalOrHigh ? '❌' : '✅'; + const statusText = hasCriticalOrHigh + ? 'Failed (Critical/High vulnerabilities found)' + : 'Passed'; + lines.push(`- **Status**: ${statusIcon} ${statusText}`); + lines.push(''); + lines.push('---'); + lines.push('*Generated by Microsoft Defender for DevOps*'); + return lines.join('\n'); +} +exports.generateMarkdownSummary = generateMarkdownSummary; +function formatScanType(scanType) { + switch (scanType.toLowerCase()) { + case 'fs': + return 'Filesystem'; + case 'image': + return 'Container Image'; + case 'model': + return 'AI Model'; + default: + return scanType; + } +} +function generateNoFindingsSummary(scanType, target) { + const lines = []; + lines.push('# Microsoft Defender for DevOps Scan Results'); + lines.push(''); + lines.push('## Summary'); + lines.push('✅ **No vulnerabilities found!**'); + lines.push(''); + lines.push('## Scan Details'); + lines.push(`- **Scan Type**: ${formatScanType(scanType)}`); + lines.push(`- **Target**: \`${target}\``); + lines.push('- **Status**: ✅ Passed'); + lines.push(''); + lines.push('---'); + lines.push('*Generated by Microsoft Defender for DevOps*'); + return lines.join('\n'); +} +exports.generateNoFindingsSummary = generateNoFindingsSummary; +function postJobSummary(sarifPath, scanType, target) { + return __awaiter(this, void 0, void 0, function* () { + try { + core.debug(`Attempting to post job summary from SARIF: ${sarifPath}`); + if (!fs.existsSync(sarifPath)) { + core.warning(`SARIF file not found at ${sarifPath}. Skipping job summary.`); + return false; + } + const sarifContent = fs.readFileSync(sarifPath, 'utf8'); + const summary = parseSarifContent(sarifContent); + core.debug(`Parsed ${summary.total} vulnerabilities from SARIF`); + const hasCriticalOrHigh = summary.critical > 0 || summary.high > 0; + let markdown; + if (summary.total === 0) { + markdown = generateNoFindingsSummary(scanType, target); + } + else { + markdown = generateMarkdownSummary(summary, scanType, target, hasCriticalOrHigh); + } + yield core.summary.addRaw(markdown).write(); + core.debug('Posted summary to GitHub Job Summary'); + return true; + } + catch (error) { + core.warning(`Failed to post job summary: ${error}`); + return false; + } + }); +} +exports.postJobSummary = postJobSummary; diff --git a/lib/v2/post.js b/lib/v2/post.js new file mode 100644 index 00000000..114788ab --- /dev/null +++ b/lib/v2/post.js @@ -0,0 +1,45 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const container_mapping_1 = require("./container-mapping"); +const defender_interface_1 = require("./defender-interface"); +function runPost() { + return __awaiter(this, void 0, void 0, function* () { + yield (0, defender_interface_1.getDefenderExecutor)(container_mapping_1.ContainerMapping).runPostJob(); + }); +} +runPost().catch((error) => { + core.debug(error); +}); diff --git a/lib/v2/pre.js b/lib/v2/pre.js new file mode 100644 index 00000000..9160a24c --- /dev/null +++ b/lib/v2/pre.js @@ -0,0 +1,45 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const container_mapping_1 = require("./container-mapping"); +const defender_interface_1 = require("./defender-interface"); +function runPre() { + return __awaiter(this, void 0, void 0, function* () { + yield (0, defender_interface_1.getDefenderExecutor)(container_mapping_1.ContainerMapping).runPreJob(); + }); +} +runPre().catch((error) => { + core.debug(error); +}); diff --git a/src/container-mapping.ts b/src/v1/container-mapping.ts similarity index 97% rename from src/container-mapping.ts rename to src/v1/container-mapping.ts index 67dc1f82..05ba0240 100644 --- a/src/container-mapping.ts +++ b/src/v1/container-mapping.ts @@ -1,292 +1,292 @@ -import { IMicrosoftSecurityDevOps } from "./msdo-interface"; -import * as https from "https"; -import * as core from '@actions/core'; -import * as exec from '@actions/exec'; -import * as os from 'os'; - -const sendReportRetryCount: number = 1; -const GetScanContextURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; -const ContainerMappingURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; - -/** - * Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run. - */ -export class ContainerMapping implements IMicrosoftSecurityDevOps { - readonly succeedOnError: boolean; - - constructor() { - this.succeedOnError = true; - } - - /** - * Container mapping pre-job commands wrapped in exception handling. - */ - public runPreJob() { - try { - core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); - this._runPreJob(); - } - catch (error) { - // Log the error - core.info("Error in Container Mapping pre-job: " + error); - } - finally { - // End the collapsible section - core.info("::endgroup::"); - } - } - - - /* - * Set the start time of the job run. - */ - private _runPreJob() { - const startTime = new Date().toISOString(); - core.saveState('PreJobStartTime', startTime); - core.info(`PreJobStartTime: ${startTime}`); - } - - /** - * Placeholder / interface satisfier for main operations - */ - public async runMain() { - // No commands - } - - /** - * Container mapping post-job commands wrapped in exception handling. - */ - public async runPostJob() { - try { - core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); - await this._runPostJob(); - } catch (error) { - // Log the error - core.info("Error in Container Mapping post-job: " + error); - } finally { - // End the collapsible section - core.info("::endgroup::"); - } - } - - /* - * Using the start time, fetch the docker events and docker images in this job run and log the encoded output - * Send the report to Defender for DevOps - */ - private async _runPostJob() { - let startTime = core.getState('PreJobStartTime'); - if (startTime.length <= 0) { - startTime = new Date(new Date().getTime() - 10000).toISOString(); - core.debug(`PreJobStartTime not defined, using now-10secs`); - } - core.info(`PreJobStartTime: ${startTime}`); - - let reportData = { - dockerVersion: "", - dockerEvents: [], - dockerImages: [] - }; - - let bearerToken: string | void = await core.getIDToken() - .then((token) => { return token; }) - .catch((error) => { - throw new Error("Unable to get token: " + error); - }); - - if (!bearerToken) { - throw new Error("Empty OIDC token received"); - } - - // Don't run the container mapping workload if this caller isn't an active customer. - var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); - if (!callerIsOnboarded) { - core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.") - return; - } - core.info("Client is onboarded for container mapping."); - - // Initialize the commands - let dockerVersionOutput = await exec.getExecOutput('docker --version'); - if (dockerVersionOutput.exitCode != 0) { - core.info(`Unable to get docker version: ${dockerVersionOutput}`); - core.info(`Skipping container mapping since docker not found/available.`); - return; - } - reportData.dockerVersion = dockerVersionOutput.stdout.trim(); - - await this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) - .catch((error) => { - throw new Error("Unable to get docker events: " + error); - }); - - await this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) - .catch((error) => { - throw new Error("Unable to get docker images: " + error); - }); - - core.debug("Finished data collection, starting API calls."); - - var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); - if (!reportSent) { - throw new Error("Unable to send report to backend service"); - }; - core.info("Container mapping data sent successfully."); - } - - /** - * Execute command and setup the listener to capture the output - * @param command Command to execute - * @param listener Listener to capture the output - * @returns a Promise - */ - private async execCommand(command: string, listener: string[]): Promise { - return exec.getExecOutput(command) - .then((result) => { - if(result.exitCode != 0) { - return Promise.reject(`Command execution failed: ${result}`); - } - result.stdout.trim().split(os.EOL).forEach(element => { - if(element.length > 0) { - listener.push(element); - } - }); - }); - } - - /** - * Sends a report to Defender for DevOps and retries on the specified count - * @param data the data to send - * @param retryCount the number of time to retry - * @param bearerToken the GitHub-generated OIDC token - * @returns a boolean Promise to indicate if the report was sent successfully or not - */ - private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise { - core.debug(`attempting to send report: ${data}`); - return await this._sendReport(data, bearerToken) - .then(() => { - return true; - }) - .catch(async (error) => { - if (retryCount == 0) { - return false; - } else { - core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); - retryCount--; - return await this.sendReport(data, bearerToken, retryCount); - } - }); - } - - /** - * Sends a report to Defender for DevOps - * @param data the data to send - * @returns a Promise - */ - private async _sendReport(data: string, bearerToken: string): Promise { - return new Promise(async (resolve, reject) => { - let apiTime = new Date().getMilliseconds(); - let options = { - method: 'POST', - timeout: 2500, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + bearerToken, - 'Content-Length': data.length - } - }; - core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); - - const req = https.request(ContainerMappingURL, options, (res) => { - let resData = ''; - res.on('data', (chunk) => { - resData += chunk.toString(); - }); - - res.on('end', () => { - core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); - core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); - core.debug('Response headers: ' + JSON.stringify(res.headers)); - if (resData.length > 0) { - core.debug('Response: ' + resData); - } - if (res.statusCode < 200 || res.statusCode >= 300) { - return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); - } - resolve(); - }); - }); - - req.on('error', (error) => { - reject(new Error(`Error calling url: ${error}`)); - }); - - req.write(data); - req.end(); - }); - } - - /** - * Queries Defender for DevOps to determine if the caller is onboarded for container mapping. - * @param retryCount the number of time to retry - * @param bearerToken the GitHub-generated OIDC token - * @returns a boolean Promise to indicate if the report was sent successfully or not - */ - private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise { - return await this._checkCallerIsCustomer(bearerToken) - .then(async (statusCode) => { - if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer. - return true; - } else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer. - return false; - } else { - core.debug(`Unexpected status code: ${statusCode}`); - return await this.retryCall(bearerToken, retryCount); - } - }) - .catch(async (error) => { - core.info(`Unexpected error: ${error}.`); - return await this.retryCall(bearerToken, retryCount); - }); - } - - private async retryCall(bearerToken: string, retryCount: number): Promise { - if (retryCount == 0) { - core.info(`All retries failed.`); - return false; - } else { - core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); - retryCount--; - return await this.checkCallerIsCustomer(bearerToken, retryCount); - } - } - - private async _checkCallerIsCustomer(bearerToken: string): Promise { - return new Promise(async (resolve, reject) => { - let options = { - method: 'GET', - timeout: 2500, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + bearerToken, - } - }; - core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); - - const req = https.request(GetScanContextURL, options, (res) => { - - res.on('end', () => { - resolve(res.statusCode); - }); - res.on('data', function(d) { - }); - }); - - req.on('error', (error) => { - reject(new Error(`Error calling url: ${error}`)); - }); - - req.end(); - }); - } - +import { IMicrosoftSecurityDevOps } from "./msdo-interface"; +import * as https from "https"; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as os from 'os'; + +const sendReportRetryCount: number = 1; +const GetScanContextURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; +const ContainerMappingURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; + +/** + * Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run. + */ +export class ContainerMapping implements IMicrosoftSecurityDevOps { + readonly succeedOnError: boolean; + + constructor() { + this.succeedOnError = true; + } + + /** + * Container mapping pre-job commands wrapped in exception handling. + */ + public runPreJob() { + try { + core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + this._runPreJob(); + } + catch (error) { + // Log the error + core.info("Error in Container Mapping pre-job: " + error); + } + finally { + // End the collapsible section + core.info("::endgroup::"); + } + } + + + /* + * Set the start time of the job run. + */ + private _runPreJob() { + const startTime = new Date().toISOString(); + core.saveState('PreJobStartTime', startTime); + core.info(`PreJobStartTime: ${startTime}`); + } + + /** + * Placeholder / interface satisfier for main operations + */ + public async runMain() { + // No commands + } + + /** + * Container mapping post-job commands wrapped in exception handling. + */ + public async runPostJob() { + try { + core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + await this._runPostJob(); + } catch (error) { + // Log the error + core.info("Error in Container Mapping post-job: " + error); + } finally { + // End the collapsible section + core.info("::endgroup::"); + } + } + + /* + * Using the start time, fetch the docker events and docker images in this job run and log the encoded output + * Send the report to Defender for DevOps + */ + private async _runPostJob() { + let startTime = core.getState('PreJobStartTime'); + if (startTime.length <= 0) { + startTime = new Date(new Date().getTime() - 10000).toISOString(); + core.debug(`PreJobStartTime not defined, using now-10secs`); + } + core.info(`PreJobStartTime: ${startTime}`); + + let reportData = { + dockerVersion: "", + dockerEvents: [], + dockerImages: [] + }; + + let bearerToken: string | void = await core.getIDToken() + .then((token) => { return token; }) + .catch((error) => { + throw new Error("Unable to get token: " + error); + }); + + if (!bearerToken) { + throw new Error("Empty OIDC token received"); + } + + // Don't run the container mapping workload if this caller isn't an active customer. + var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); + if (!callerIsOnboarded) { + core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.") + return; + } + core.info("Client is onboarded for container mapping."); + + // Initialize the commands + let dockerVersionOutput = await exec.getExecOutput('docker --version'); + if (dockerVersionOutput.exitCode != 0) { + core.info(`Unable to get docker version: ${dockerVersionOutput}`); + core.info(`Skipping container mapping since docker not found/available.`); + return; + } + reportData.dockerVersion = dockerVersionOutput.stdout.trim(); + + await this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) + .catch((error) => { + throw new Error("Unable to get docker events: " + error); + }); + + await this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) + .catch((error) => { + throw new Error("Unable to get docker images: " + error); + }); + + core.debug("Finished data collection, starting API calls."); + + var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); + if (!reportSent) { + throw new Error("Unable to send report to backend service"); + }; + core.info("Container mapping data sent successfully."); + } + + /** + * Execute command and setup the listener to capture the output + * @param command Command to execute + * @param listener Listener to capture the output + * @returns a Promise + */ + private async execCommand(command: string, listener: string[]): Promise { + return exec.getExecOutput(command) + .then((result) => { + if(result.exitCode != 0) { + return Promise.reject(`Command execution failed: ${result}`); + } + result.stdout.trim().split(os.EOL).forEach(element => { + if(element.length > 0) { + listener.push(element); + } + }); + }); + } + + /** + * Sends a report to Defender for DevOps and retries on the specified count + * @param data the data to send + * @param retryCount the number of time to retry + * @param bearerToken the GitHub-generated OIDC token + * @returns a boolean Promise to indicate if the report was sent successfully or not + */ + private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise { + core.debug(`attempting to send report: ${data}`); + return await this._sendReport(data, bearerToken) + .then(() => { + return true; + }) + .catch(async (error) => { + if (retryCount == 0) { + return false; + } else { + core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); + retryCount--; + return await this.sendReport(data, bearerToken, retryCount); + } + }); + } + + /** + * Sends a report to Defender for DevOps + * @param data the data to send + * @returns a Promise + */ + private async _sendReport(data: string, bearerToken: string): Promise { + return new Promise(async (resolve, reject) => { + let apiTime = new Date().getMilliseconds(); + let options = { + method: 'POST', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + 'Content-Length': data.length + } + }; + core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); + + const req = https.request(ContainerMappingURL, options, (res) => { + let resData = ''; + res.on('data', (chunk) => { + resData += chunk.toString(); + }); + + res.on('end', () => { + core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); + core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); + core.debug('Response headers: ' + JSON.stringify(res.headers)); + if (resData.length > 0) { + core.debug('Response: ' + resData); + } + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); + } + resolve(); + }); + }); + + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + + req.write(data); + req.end(); + }); + } + + /** + * Queries Defender for DevOps to determine if the caller is onboarded for container mapping. + * @param retryCount the number of time to retry + * @param bearerToken the GitHub-generated OIDC token + * @returns a boolean Promise to indicate if the report was sent successfully or not + */ + private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise { + return await this._checkCallerIsCustomer(bearerToken) + .then(async (statusCode) => { + if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer. + return true; + } else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer. + return false; + } else { + core.debug(`Unexpected status code: ${statusCode}`); + return await this.retryCall(bearerToken, retryCount); + } + }) + .catch(async (error) => { + core.info(`Unexpected error: ${error}.`); + return await this.retryCall(bearerToken, retryCount); + }); + } + + private async retryCall(bearerToken: string, retryCount: number): Promise { + if (retryCount == 0) { + core.info(`All retries failed.`); + return false; + } else { + core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); + retryCount--; + return await this.checkCallerIsCustomer(bearerToken, retryCount); + } + } + + private async _checkCallerIsCustomer(bearerToken: string): Promise { + return new Promise(async (resolve, reject) => { + let options = { + method: 'GET', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + } + }; + core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); + + const req = https.request(GetScanContextURL, options, (res) => { + + res.on('end', () => { + resolve(res.statusCode); + }); + res.on('data', function(d) { + }); + }); + + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + + req.end(); + }); + } + } \ No newline at end of file diff --git a/src/main.ts b/src/v1/main.ts similarity index 96% rename from src/main.ts rename to src/v1/main.ts index 1f45f9d1..d843109d 100644 --- a/src/main.ts +++ b/src/v1/main.ts @@ -1,34 +1,34 @@ -import * as core from '@actions/core'; -import { MicrosoftSecurityDevOps } from './msdo'; -import { getExecutor } from './msdo-interface'; -import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; -import { Tools } from './msdo-helpers'; - -async function runMain() { - if (shouldRunMain()) - { - await getExecutor(MicrosoftSecurityDevOps).runMain(); - } - else { - console.log("Scanning is not enabled. Skipping..."); - } -} - -runMain().catch(error => { - core.setFailed(error); -}); - -/** - * Returns false if the 'tools' input is specified and the only tool on the list is 'container-mapping'. - * This is because the MicrosoftSecurityDevOps executer does not have a workload for the container-mapping tool. -*/ -function shouldRunMain() { - let toolsString: string = core.getInput('tools'); - if (!common.isNullOrWhiteSpace(toolsString)) { - let tools = toolsString.split(','); - if (tools.length == 1 && tools[0].trim() == Tools.ContainerMapping) { - return false; - } - } - return true; +import * as core from '@actions/core'; +import { MicrosoftSecurityDevOps } from './msdo'; +import { getExecutor } from './msdo-interface'; +import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; +import { Tools } from './msdo-helpers'; + +async function runMain() { + if (shouldRunMain()) + { + await getExecutor(MicrosoftSecurityDevOps).runMain(); + } + else { + console.log("Scanning is not enabled. Skipping..."); + } +} + +runMain().catch(error => { + core.setFailed(error); +}); + +/** + * Returns false if the 'tools' input is specified and the only tool on the list is 'container-mapping'. + * This is because the MicrosoftSecurityDevOps executer does not have a workload for the container-mapping tool. +*/ +function shouldRunMain() { + let toolsString: string = core.getInput('tools'); + if (!common.isNullOrWhiteSpace(toolsString)) { + let tools = toolsString.split(','); + if (tools.length == 1 && tools[0].trim() == Tools.ContainerMapping) { + return false; + } + } + return true; } \ No newline at end of file diff --git a/src/msdo-helpers.ts b/src/v1/msdo-helpers.ts similarity index 96% rename from src/msdo-helpers.ts rename to src/v1/msdo-helpers.ts index 72b13f14..39959174 100644 --- a/src/msdo-helpers.ts +++ b/src/v1/msdo-helpers.ts @@ -1,96 +1,96 @@ -import os from 'os'; -import { Writable } from "stream"; - -/** - * Enum for the possible inputs for the task (specified in action.yml) - */ -export enum Inputs { - Command = 'command', - Config = 'config', - Policy = 'policy', - Categories = 'categories', - Languages = 'languages', - Tools = 'tools', - IncludeTools = 'includeTools', - ExistingFilename = 'existingFilename' -} - -/** - * Enum for the runner of the action. - */ -export enum RunnerType { - Main = 'main', - Pre = 'pre', - Post = 'post' -} - -/* -* Enum for the possible values for the Inputs.Tools (specified in action.yml) -*/ -export enum Tools { - Bandit = 'bandit', - Binskim = 'binskim', - Checkov = 'checkov', - ContainerMapping = 'container-mapping', - ESLint = 'eslint', - TemplateAnalyzer = 'templateanalyzer', - Terrascan = 'terrascan', - Trivy = 'trivy' -} - -/** - * Enum for defining constants used in the task. - */ -export enum Constants { - Unknown = "unknown", - PreJobStartTime = "PREJOBSTARTTIME" -} - -/** - * Encodes a string to base64. - * - * @param str - The string to encode. - * @returns The base64 encoded string. - */ -export const encode = (str: string):string => Buffer.from(str, 'binary').toString('base64'); - -/** - * Returns the encoded content of the Docker version, Docker events, and Docker images in the pre-defined format - - * DockerVersion - * Version: TaskVersion - * Events: - * DockerEvents - * Images: - * DockerImages - * - * @param dockerVersion - The version of Docker. - * @param dockerEvents - The Docker events. - * @param dockerImages - The Docker images. - * @param taskVersion - Optional version of the task. Defaults to the version in the action.yml file. - * @param sectionDelim - Optional delimiter to separate sections in the encoded content. Defaults to ":::". - * @returns The encoded content of the Docker version, Docker events, and Docker images. - */ -export function getEncodedContent( - dockerVersion: string, - dockerEvents: string, - dockerImages: string -): string { - let data : string[] = []; - data.push("DockerVersion: " + dockerVersion); - data.push("DockerEvents:"); - data.push(dockerEvents); - data.push("DockerImages:"); - data.push(dockerImages); - return encode(data.join(os.EOL)); -} - -/** - * Writes the specified data to the specified output stream, followed by the platform-specific end-of-line character. - * If no output stream is specified, the data is written to the standard output stream. - * - * @param data - The data to write to the output stream. - * @param outStream - Optional. The output stream to write the data to. Defaults to the standard output stream. - */ -export function writeToOutStream(data: string, outStream: Writable = process.stdout): void { - outStream.write(data.trim() + os.EOL); +import os from 'os'; +import { Writable } from "stream"; + +/** + * Enum for the possible inputs for the task (specified in action.yml) + */ +export enum Inputs { + Command = 'command', + Config = 'config', + Policy = 'policy', + Categories = 'categories', + Languages = 'languages', + Tools = 'tools', + IncludeTools = 'includeTools', + ExistingFilename = 'existingFilename' +} + +/** + * Enum for the runner of the action. + */ +export enum RunnerType { + Main = 'main', + Pre = 'pre', + Post = 'post' +} + +/* +* Enum for the possible values for the Inputs.Tools (specified in action.yml) +*/ +export enum Tools { + Bandit = 'bandit', + Binskim = 'binskim', + Checkov = 'checkov', + ContainerMapping = 'container-mapping', + ESLint = 'eslint', + TemplateAnalyzer = 'templateanalyzer', + Terrascan = 'terrascan', + Trivy = 'trivy' +} + +/** + * Enum for defining constants used in the task. + */ +export enum Constants { + Unknown = "unknown", + PreJobStartTime = "PREJOBSTARTTIME" +} + +/** + * Encodes a string to base64. + * + * @param str - The string to encode. + * @returns The base64 encoded string. + */ +export const encode = (str: string):string => Buffer.from(str, 'binary').toString('base64'); + +/** + * Returns the encoded content of the Docker version, Docker events, and Docker images in the pre-defined format - + * DockerVersion + * Version: TaskVersion + * Events: + * DockerEvents + * Images: + * DockerImages + * + * @param dockerVersion - The version of Docker. + * @param dockerEvents - The Docker events. + * @param dockerImages - The Docker images. + * @param taskVersion - Optional version of the task. Defaults to the version in the action.yml file. + * @param sectionDelim - Optional delimiter to separate sections in the encoded content. Defaults to ":::". + * @returns The encoded content of the Docker version, Docker events, and Docker images. + */ +export function getEncodedContent( + dockerVersion: string, + dockerEvents: string, + dockerImages: string +): string { + let data : string[] = []; + data.push("DockerVersion: " + dockerVersion); + data.push("DockerEvents:"); + data.push(dockerEvents); + data.push("DockerImages:"); + data.push(dockerImages); + return encode(data.join(os.EOL)); +} + +/** + * Writes the specified data to the specified output stream, followed by the platform-specific end-of-line character. + * If no output stream is specified, the data is written to the standard output stream. + * + * @param data - The data to write to the output stream. + * @param outStream - Optional. The output stream to write the data to. Defaults to the standard output stream. + */ +export function writeToOutStream(data: string, outStream: Writable = process.stdout): void { + outStream.write(data.trim() + os.EOL); } \ No newline at end of file diff --git a/src/msdo-interface.ts b/src/v1/msdo-interface.ts similarity index 97% rename from src/msdo-interface.ts rename to src/v1/msdo-interface.ts index af50977e..9a02bad5 100644 --- a/src/msdo-interface.ts +++ b/src/v1/msdo-interface.ts @@ -1,29 +1,29 @@ -/* -* Interface for the MicrosoftSecurityDevOps task -*/ -export interface IMicrosoftSecurityDevOps { - readonly succeedOnError: boolean; - /* param source - The source of the task: main, pre, or post. */ - runPreJob(): any; - runMain(): any; - runPostJob(): any; -} - -/** - * Factory interface for creating instances of the `IMicrosoftSecurityDevOps` interface. - * This factory enforces the inputs that can be used for creation of the `IMicrosoftSecurityDevOps` instances. - */ -export interface IMicrosoftSecurityDevOpsFactory { - new (): IMicrosoftSecurityDevOps; -} - -/** - * Returns an instance of IMicrosoftSecurityDevOps based on the input runner and command type. - * (This is used to enforce strong typing for the inputs for the runner). - * @param runner - The runner to use to create the instance of IMicrosoftSecurityDevOps. - * @param commandType - The input command type. - * @returns An instance of IMicrosoftSecurityDevOps. - */ -export function getExecutor(runner: IMicrosoftSecurityDevOpsFactory): IMicrosoftSecurityDevOps { - return new runner(); +/* +* Interface for the MicrosoftSecurityDevOps task +*/ +export interface IMicrosoftSecurityDevOps { + readonly succeedOnError: boolean; + /* param source - The source of the task: main, pre, or post. */ + runPreJob(): any; + runMain(): any; + runPostJob(): any; +} + +/** + * Factory interface for creating instances of the `IMicrosoftSecurityDevOps` interface. + * This factory enforces the inputs that can be used for creation of the `IMicrosoftSecurityDevOps` instances. + */ +export interface IMicrosoftSecurityDevOpsFactory { + new (): IMicrosoftSecurityDevOps; +} + +/** + * Returns an instance of IMicrosoftSecurityDevOps based on the input runner and command type. + * (This is used to enforce strong typing for the inputs for the runner). + * @param runner - The runner to use to create the instance of IMicrosoftSecurityDevOps. + * @param commandType - The input command type. + * @returns An instance of IMicrosoftSecurityDevOps. + */ +export function getExecutor(runner: IMicrosoftSecurityDevOpsFactory): IMicrosoftSecurityDevOps { + return new runner(); } \ No newline at end of file diff --git a/src/msdo.ts b/src/v1/msdo.ts similarity index 97% rename from src/msdo.ts rename to src/v1/msdo.ts index de7afadb..8d9680f1 100644 --- a/src/msdo.ts +++ b/src/v1/msdo.ts @@ -1,108 +1,108 @@ -import * as core from '@actions/core'; -import { IMicrosoftSecurityDevOps } from './msdo-interface'; -import { Tools } from './msdo-helpers'; -import * as client from '@microsoft/security-devops-actions-toolkit/msdo-client'; -import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; - -/* -* Microsoft Security DevOps analyzers runner. -*/ -export class MicrosoftSecurityDevOps implements IMicrosoftSecurityDevOps { - readonly succeedOnError: boolean; - - constructor() { - this.succeedOnError = false; - } - - public async runPreJob() { - // No pre-job commands yet - } - - public async runPostJob() { - // No post-job commands yet - } - - public async runMain() { - core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...'); - - let args: string[] = undefined; - - // Check job type - might be existing file - let existingFilename = core.getInput('existingFilename'); - if (!common.isNullOrWhiteSpace(existingFilename)) { - args = ['upload', '--file', existingFilename]; - } - - // Nope, run the tool as intended - else { - args = ['run']; - - let config: string = core.getInput('config'); - if (!common.isNullOrWhiteSpace(config)) { - args.push('-c'); - args.push(config); - } - - let policy: string = core.getInput('policy'); - if (common.isNullOrWhiteSpace(policy)) { - policy = "GitHub"; - } - - args.push('-p'); - args.push(policy); - - let categoriesString: string = core.getInput('categories'); - if (!common.isNullOrWhiteSpace(categoriesString)) { - args.push('--categories'); - let categories = categoriesString.split(','); - for (let i = 0; i < categories.length; i++) { - let category = categories[i]; - if (!common.isNullOrWhiteSpace(category)) { - args.push(category.trim()); - } - } - } - - let languagesString: string = core.getInput('languages'); - if (!common.isNullOrWhiteSpace(languagesString)) { - args.push('--languages'); - let languages = languagesString.split(','); - for (let i = 0; i < languages.length; i++) { - let language = languages[i]; - if (!common.isNullOrWhiteSpace(language)) { - args.push(language.trim()); - } - } - } - - let toolsString: string = core.getInput('tools'); - let includedTools = []; - if (!common.isNullOrWhiteSpace(toolsString)) { - let tools = toolsString.split(','); - for (let i = 0; i < tools.length; i++) { - let tool = tools[i]; - let toolTrimmed = tool.trim(); - if (!common.isNullOrWhiteSpace(tool) - && tool != Tools.ContainerMapping // This tool is not handled by this executor - && includedTools.indexOf(toolTrimmed) == -1) { - if (includedTools.length == 0) { - args.push('--tool'); - } - args.push(toolTrimmed); - includedTools.push(toolTrimmed); - } - } - } - - args.push('--github'); - } - - let breakOnDetections: string = core.getInput('break-on-detections'); - if (breakOnDetections && breakOnDetections.trim().toUpperCase() === 'TRUE') { - process.env.MSDO_BREAK = 'true'; - core.debug('break-on-detections is enabled, set MSDO_BREAK=true'); - } - - await client.run(args, 'microsoft/security-devops-action'); - } +import * as core from '@actions/core'; +import { IMicrosoftSecurityDevOps } from './msdo-interface'; +import { Tools } from './msdo-helpers'; +import * as client from '@microsoft/security-devops-actions-toolkit/msdo-client'; +import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; + +/* +* Microsoft Security DevOps analyzers runner. +*/ +export class MicrosoftSecurityDevOps implements IMicrosoftSecurityDevOps { + readonly succeedOnError: boolean; + + constructor() { + this.succeedOnError = false; + } + + public async runPreJob() { + // No pre-job commands yet + } + + public async runPostJob() { + // No post-job commands yet + } + + public async runMain() { + core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...'); + + let args: string[] = undefined; + + // Check job type - might be existing file + let existingFilename = core.getInput('existingFilename'); + if (!common.isNullOrWhiteSpace(existingFilename)) { + args = ['upload', '--file', existingFilename]; + } + + // Nope, run the tool as intended + else { + args = ['run']; + + let config: string = core.getInput('config'); + if (!common.isNullOrWhiteSpace(config)) { + args.push('-c'); + args.push(config); + } + + let policy: string = core.getInput('policy'); + if (common.isNullOrWhiteSpace(policy)) { + policy = "GitHub"; + } + + args.push('-p'); + args.push(policy); + + let categoriesString: string = core.getInput('categories'); + if (!common.isNullOrWhiteSpace(categoriesString)) { + args.push('--categories'); + let categories = categoriesString.split(','); + for (let i = 0; i < categories.length; i++) { + let category = categories[i]; + if (!common.isNullOrWhiteSpace(category)) { + args.push(category.trim()); + } + } + } + + let languagesString: string = core.getInput('languages'); + if (!common.isNullOrWhiteSpace(languagesString)) { + args.push('--languages'); + let languages = languagesString.split(','); + for (let i = 0; i < languages.length; i++) { + let language = languages[i]; + if (!common.isNullOrWhiteSpace(language)) { + args.push(language.trim()); + } + } + } + + let toolsString: string = core.getInput('tools'); + let includedTools = []; + if (!common.isNullOrWhiteSpace(toolsString)) { + let tools = toolsString.split(','); + for (let i = 0; i < tools.length; i++) { + let tool = tools[i]; + let toolTrimmed = tool.trim(); + if (!common.isNullOrWhiteSpace(tool) + && tool != Tools.ContainerMapping // This tool is not handled by this executor + && includedTools.indexOf(toolTrimmed) == -1) { + if (includedTools.length == 0) { + args.push('--tool'); + } + args.push(toolTrimmed); + includedTools.push(toolTrimmed); + } + } + } + + args.push('--github'); + } + + let breakOnDetections: string = core.getInput('break-on-detections'); + if (breakOnDetections && breakOnDetections.trim().toUpperCase() === 'TRUE') { + process.env.MSDO_BREAK = 'true'; + core.debug('break-on-detections is enabled, set MSDO_BREAK=true'); + } + + await client.run(args, 'microsoft/security-devops-action'); + } } \ No newline at end of file diff --git a/src/post.ts b/src/v1/post.ts similarity index 96% rename from src/post.ts rename to src/v1/post.ts index ab75224f..36946894 100644 --- a/src/post.ts +++ b/src/v1/post.ts @@ -1,11 +1,11 @@ -import * as core from '@actions/core'; -import { ContainerMapping } from './container-mapping'; -import { getExecutor } from './msdo-interface'; - -async function runPost() { - await getExecutor(ContainerMapping).runPostJob(); -} - -runPost().catch((error) => { - core.debug(error); +import * as core from '@actions/core'; +import { ContainerMapping } from './container-mapping'; +import { getExecutor } from './msdo-interface'; + +async function runPost() { + await getExecutor(ContainerMapping).runPostJob(); +} + +runPost().catch((error) => { + core.debug(error); }); \ No newline at end of file diff --git a/src/pre.ts b/src/v1/pre.ts similarity index 96% rename from src/pre.ts rename to src/v1/pre.ts index f717e43a..602af8a8 100644 --- a/src/pre.ts +++ b/src/v1/pre.ts @@ -1,11 +1,11 @@ -import * as core from '@actions/core'; -import { ContainerMapping } from './container-mapping'; -import { getExecutor } from './msdo-interface'; - -async function runPre() { - await getExecutor(ContainerMapping).runPreJob(); -} - -runPre().catch((error) => { - core.debug(error); +import * as core from '@actions/core'; +import { ContainerMapping } from './container-mapping'; +import { getExecutor } from './msdo-interface'; + +async function runPre() { + await getExecutor(ContainerMapping).runPreJob(); +} + +runPre().catch((error) => { + core.debug(error); }); \ No newline at end of file diff --git a/src/v2/container-mapping.ts b/src/v2/container-mapping.ts new file mode 100644 index 00000000..cf52c5ec --- /dev/null +++ b/src/v2/container-mapping.ts @@ -0,0 +1,292 @@ +import { IMicrosoftDefenderCLI } from "./defender-interface"; +import * as https from "https"; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as os from 'os'; + +const sendReportRetryCount: number = 1; +const GetScanContextURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; +const ContainerMappingURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; + +/** + * Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run. + */ +export class ContainerMapping implements IMicrosoftDefenderCLI { + readonly succeedOnError: boolean; + + constructor() { + this.succeedOnError = true; + } + + /** + * Container mapping pre-job commands wrapped in exception handling. + */ + public runPreJob() { + try { + core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + this._runPreJob(); + } + catch (error) { + // Log the error + core.info("Error in Container Mapping pre-job: " + error); + } + finally { + // End the collapsible section + core.info("::endgroup::"); + } + } + + + /* + * Set the start time of the job run. + */ + private _runPreJob() { + const startTime = new Date().toISOString(); + core.saveState('PreJobStartTime', startTime); + core.info(`PreJobStartTime: ${startTime}`); + } + + /** + * Placeholder / interface satisfier for main operations + */ + public async runMain() { + // No commands + } + + /** + * Container mapping post-job commands wrapped in exception handling. + */ + public async runPostJob() { + try { + core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); + await this._runPostJob(); + } catch (error) { + // Log the error + core.info("Error in Container Mapping post-job: " + error); + } finally { + // End the collapsible section + core.info("::endgroup::"); + } + } + + /* + * Using the start time, fetch the docker events and docker images in this job run and log the encoded output + * Send the report to Defender for DevOps + */ + private async _runPostJob() { + let startTime = core.getState('PreJobStartTime'); + if (startTime.length <= 0) { + startTime = new Date(new Date().getTime() - 10000).toISOString(); + core.debug(`PreJobStartTime not defined, using now-10secs`); + } + core.info(`PreJobStartTime: ${startTime}`); + + let reportData = { + dockerVersion: "", + dockerEvents: [], + dockerImages: [] + }; + + let bearerToken: string | void = await core.getIDToken() + .then((token) => { return token; }) + .catch((error) => { + throw new Error("Unable to get token: " + error); + }); + + if (!bearerToken) { + throw new Error("Empty OIDC token received"); + } + + // Don't run the container mapping workload if this caller isn't an active customer. + var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); + if (!callerIsOnboarded) { + core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.") + return; + } + core.info("Client is onboarded for container mapping."); + + // Initialize the commands + let dockerVersionOutput = await exec.getExecOutput('docker --version'); + if (dockerVersionOutput.exitCode != 0) { + core.info(`Unable to get docker version: ${dockerVersionOutput}`); + core.info(`Skipping container mapping since docker not found/available.`); + return; + } + reportData.dockerVersion = dockerVersionOutput.stdout.trim(); + + await this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) + .catch((error) => { + throw new Error("Unable to get docker events: " + error); + }); + + await this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) + .catch((error) => { + throw new Error("Unable to get docker images: " + error); + }); + + core.debug("Finished data collection, starting API calls."); + + var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); + if (!reportSent) { + throw new Error("Unable to send report to backend service"); + }; + core.info("Container mapping data sent successfully."); + } + + /** + * Execute command and setup the listener to capture the output + * @param command Command to execute + * @param listener Listener to capture the output + * @returns a Promise + */ + private async execCommand(command: string, listener: string[]): Promise { + return exec.getExecOutput(command) + .then((result) => { + if(result.exitCode != 0) { + return Promise.reject(`Command execution failed: ${result}`); + } + result.stdout.trim().split(os.EOL).forEach(element => { + if(element.length > 0) { + listener.push(element); + } + }); + }); + } + + /** + * Sends a report to Defender for DevOps and retries on the specified count + * @param data the data to send + * @param retryCount the number of time to retry + * @param bearerToken the GitHub-generated OIDC token + * @returns a boolean Promise to indicate if the report was sent successfully or not + */ + private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise { + core.debug(`attempting to send report: ${data}`); + return await this._sendReport(data, bearerToken) + .then(() => { + return true; + }) + .catch(async (error) => { + if (retryCount == 0) { + return false; + } else { + core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); + retryCount--; + return await this.sendReport(data, bearerToken, retryCount); + } + }); + } + + /** + * Sends a report to Defender for DevOps + * @param data the data to send + * @returns a Promise + */ + private async _sendReport(data: string, bearerToken: string): Promise { + return new Promise(async (resolve, reject) => { + let apiTime = new Date().getMilliseconds(); + let options = { + method: 'POST', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + 'Content-Length': data.length + } + }; + core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); + + const req = https.request(ContainerMappingURL, options, (res) => { + let resData = ''; + res.on('data', (chunk) => { + resData += chunk.toString(); + }); + + res.on('end', () => { + core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); + core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); + core.debug('Response headers: ' + JSON.stringify(res.headers)); + if (resData.length > 0) { + core.debug('Response: ' + resData); + } + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); + } + resolve(); + }); + }); + + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + + req.write(data); + req.end(); + }); + } + + /** + * Queries Defender for DevOps to determine if the caller is onboarded for container mapping. + * @param retryCount the number of time to retry + * @param bearerToken the GitHub-generated OIDC token + * @returns a boolean Promise to indicate if the report was sent successfully or not + */ + private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise { + return await this._checkCallerIsCustomer(bearerToken) + .then(async (statusCode) => { + if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer. + return true; + } else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer. + return false; + } else { + core.debug(`Unexpected status code: ${statusCode}`); + return await this.retryCall(bearerToken, retryCount); + } + }) + .catch(async (error) => { + core.info(`Unexpected error: ${error}.`); + return await this.retryCall(bearerToken, retryCount); + }); + } + + private async retryCall(bearerToken: string, retryCount: number): Promise { + if (retryCount == 0) { + core.info(`All retries failed.`); + return false; + } else { + core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); + retryCount--; + return await this.checkCallerIsCustomer(bearerToken, retryCount); + } + } + + private async _checkCallerIsCustomer(bearerToken: string): Promise { + return new Promise(async (resolve, reject) => { + let options = { + method: 'GET', + timeout: 2500, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + bearerToken, + } + }; + core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); + + const req = https.request(GetScanContextURL, options, (res) => { + + res.on('end', () => { + resolve(res.statusCode); + }); + res.on('data', function(d) { + }); + }); + + req.on('error', (error) => { + reject(new Error(`Error calling url: ${error}`)); + }); + + req.end(); + }); + } + +} diff --git a/src/v2/defender-cli.ts b/src/v2/defender-cli.ts new file mode 100644 index 00000000..63ebc642 --- /dev/null +++ b/src/v2/defender-cli.ts @@ -0,0 +1,229 @@ +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as path from 'path'; +import { ScanType, Inputs, validateScanType, validateImageName, validateModelPath, validateFileSystemPath, parseAdditionalArgs, setupDebugLogging } from './defender-helpers'; +import { IMicrosoftDefenderCLI } from './defender-interface'; +import { scanDirectory, scanImage } from './defender-client'; +import { postJobSummary } from './job-summary'; + +/* + * Class for Microsoft Defender CLI functionality. + * Mirrors AzDevOps v2's defender-cli.ts, adapted for GitHub Actions. + */ +export class MicrosoftDefenderCLI implements IMicrosoftDefenderCLI { + readonly succeedOnError: boolean; + private prSummaryEnabled: boolean = true; + + constructor() { + this.succeedOnError = false; + } + + public async runPreJob() { + // No pre-job commands for Defender CLI scanning + } + + public async runPostJob() { + // No post-job commands for Defender CLI scanning + } + + public async runMain() { + await this.runDefenderCLI(); + } + + private async runDefenderCLI() { + // Get debug setting early to enable verbose logging + const debugInput = core.getInput(Inputs.Debug); + const debug = debugInput ? debugInput.toLowerCase() === 'true' : false; + if (debug) { + setupDebugLogging(true); + core.debug('Debug logging enabled'); + } + + // Get and validate scan type using 'command' input with 'fs' as default + const command: string = core.getInput(Inputs.Command) || 'fs'; + const scanType = validateScanType(command); + + // Get pr-summary flag (defaults to true) + const prSummaryInput = core.getInput(Inputs.PrSummary); + this.prSummaryEnabled = prSummaryInput ? prSummaryInput.toLowerCase() !== 'false' : true; + core.debug(`PR Summary enabled: ${this.prSummaryEnabled}`); + + // Get and parse additional arguments + const argsInput = core.getInput(Inputs.Args) || ''; + let additionalArgs = parseAdditionalArgs(argsInput); + + let target: string; + + // Get target based on scan type and validate + switch (scanType) { + case ScanType.FileSystem: + const fileSystemPath = core.getInput(Inputs.FileSystemPath) || + process.env['GITHUB_WORKSPACE'] || + process.cwd(); + target = validateFileSystemPath(fileSystemPath); + core.debug(`Filesystem scan using directory: ${target}`); + break; + + case ScanType.Image: + const imageName = core.getInput(Inputs.ImageName); + if (!imageName) { + throw new Error('Image name is required for image scan'); + } + target = validateImageName(imageName); + break; + + case ScanType.Model: + const modelPath = core.getInput(Inputs.ModelPath); + if (!modelPath) { + throw new Error('Model path is required for model scan'); + } + target = validateModelPath(modelPath); + break; + + default: + throw new Error(`Unsupported scan type: ${scanType}`); + } + + // Handle break on critical vulnerability + const breakInput = core.getInput(Inputs.Break); + const breakOnCritical = breakInput ? breakInput.toLowerCase() === 'true' : false; + + // Remove --defender-break from additional args if manually added + additionalArgs = additionalArgs.filter(arg => arg !== '--defender-break'); + + if (breakOnCritical) { + additionalArgs.push('--defender-break'); + core.debug('Break on critical vulnerability enabled: adding --defender-break flag'); + } + + // Remove --defender-debug from additional args if manually added + additionalArgs = additionalArgs.filter(arg => arg !== '--defender-debug'); + + if (debug) { + additionalArgs.push('--defender-debug'); + core.debug('Debug mode enabled: adding --defender-debug flag'); + } + + // Determine successful exit codes + let successfulExitCodes: number[] = [0]; + + // Generate output path + const outputPath = path.join( + process.env['RUNNER_TEMP'] || process.cwd(), + 'defender.sarif' + ); + + // Get policy from input, default to 'github' + const policyInput: string = core.getInput(Inputs.Policy) || 'github'; + let policy: string; + if (policyInput === 'none') { + policy = ''; + } else { + policy = policyInput; + } + + // Log scan information + core.debug(`Scan Type: ${scanType}`); + core.debug(`Target: ${target}`); + core.debug(`Policy: ${policy}`); + core.debug(`Output Path: ${outputPath}`); + if (additionalArgs.length > 0) { + core.debug(`Additional Arguments: ${additionalArgs.join(' ')}`); + } + + // Set environment variable to indicate execution via extension + process.env['Defender_Extension'] = 'true'; + core.debug('Environment variable set: Defender_Extension=true'); + + try { + switch (scanType) { + case ScanType.FileSystem: + await scanDirectory(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + + case ScanType.Image: + await scanImage(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + + case ScanType.Model: + await this.runModelScan(target, policy, outputPath, successfulExitCodes, additionalArgs); + break; + } + + if (this.prSummaryEnabled) { + core.debug('Posting job summary...'); + await postJobSummary(outputPath, scanType, target); + } + } catch (error) { + // Still try to post summary on error if enabled (for partial results) + if (this.prSummaryEnabled) { + try { + await postJobSummary(outputPath, scanType, target); + } catch (summaryError) { + core.debug(`Failed to post summary after error: ${summaryError}`); + } + } + + core.error(`Defender CLI execution failed: ${error}`); + throw error; + } + } + + /** + * Runs a model scan using the Defender CLI directly. + * This is needed because the defender-client doesn't export a scanModel() function. + */ + private async runModelScan( + modelPath: string, + policy: string, + outputPath: string, + successfulExitCodes: number[], + additionalArgs: string[] + ): Promise { + const cliFilePath = process.env['DEFENDER_FILEPATH']; + + if (!cliFilePath) { + throw new Error('DEFENDER_FILEPATH environment variable is not set. Defender CLI may not be installed.'); + } + + const args = [ + 'scan', + 'model', + modelPath, + ]; + + if (policy) { + args.push('--defender-policy', policy); + } + + args.push('--defender-output', outputPath); + + if (additionalArgs && additionalArgs.length > 0) { + args.push(...additionalArgs); + core.debug(`Appending additional arguments: ${additionalArgs.join(' ')}`); + } + + // Check if debug is enabled + const isDebug = process.env['RUNNER_DEBUG'] === '1' || core.isDebug(); + if (isDebug && !args.includes('--defender-debug')) { + args.push('--defender-debug'); + } + + core.debug('Running Microsoft Defender CLI for model scan...'); + const exitCode = await exec.exec(cliFilePath, args, { + ignoreReturnCode: true + }); + + let success = false; + for (const successCode of successfulExitCodes) { + if (exitCode === successCode) { + success = true; + break; + } + } + + if (!success) { + throw new Error(`Defender CLI exited with an error exit code: ${exitCode}`); + } + } +} diff --git a/src/v2/defender-client.ts b/src/v2/defender-client.ts new file mode 100644 index 00000000..ac7743d4 --- /dev/null +++ b/src/v2/defender-client.ts @@ -0,0 +1,143 @@ +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as installer from './defender-installer'; + +/** + * Scans a local filesystem directory for security vulnerabilities. + */ +export async function scanDirectory( + directoryPath: string, + policy?: string, + outputPath?: string, + successfulExitCodes?: number[], + additionalArgs?: string[] +): Promise { + await scan('fs', directoryPath, policy, outputPath, successfulExitCodes, additionalArgs); +} + +/** + * Scans a container image for security vulnerabilities. + */ +export async function scanImage( + imageName: string, + policy?: string, + outputPath?: string, + successfulExitCodes?: number[], + additionalArgs?: string[] +): Promise { + await scan('image', imageName, policy, outputPath, successfulExitCodes, additionalArgs); +} + +/** + * Generic scan function used by scanDirectory and scanImage. + */ +async function scan( + scanType: string, + target: string, + policy?: string, + outputPath?: string, + successfulExitCodes?: number[], + additionalArgs?: string[] +): Promise { + const resolvedPolicy = policy || 'mdc'; + const resolvedOutputPath = outputPath || path.join( + process.env['RUNNER_TEMP'] || process.cwd(), + 'defender.sarif' + ); + + const inputArgs: string[] = [ + 'scan', + scanType, + target, + '--defender-policy', + resolvedPolicy, + '--defender-output', + resolvedOutputPath + ]; + + if (additionalArgs && additionalArgs.length > 0) { + inputArgs.push(...additionalArgs); + } + + await runDefenderCli(inputArgs, successfulExitCodes); +} + +/** + * Executes the Defender CLI with the given arguments. + */ +async function runDefenderCli( + inputArgs: string[], + successfulExitCodes?: number[] +): Promise { + await setupEnvironment(); + + const cliFilePath = getCliFilePath(); + if (!cliFilePath) { + throw new Error('DEFENDER_FILEPATH environment variable is not set. Defender CLI may not be installed.'); + } + + core.debug(`Running Defender CLI: ${cliFilePath} ${inputArgs.join(' ')}`); + + // Add debug flag if runner debug is enabled + const isDebug = process.env['RUNNER_DEBUG'] === '1' || core.isDebug(); + if (isDebug && !inputArgs.includes('--defender-debug')) { + inputArgs.push('--defender-debug'); + } + + const exitCode = await exec.exec(cliFilePath, inputArgs, { + ignoreReturnCode: true + }); + + const validExitCodes = successfulExitCodes || [0]; + + if (!validExitCodes.includes(exitCode)) { + throw new Error(`Defender CLI exited with an error exit code: ${exitCode}`); + } + + core.debug(`Defender CLI completed successfully with exit code: ${exitCode}`); +} + +/** + * Sets up the environment for the Defender CLI. + */ +async function setupEnvironment(): Promise { + const toolCacheDir = process.env['RUNNER_TOOL_CACHE'] || path.join(os.homedir(), '.defender'); + const defenderDir = path.join(toolCacheDir, '_defender'); + + if (!fs.existsSync(defenderDir)) { + fs.mkdirSync(defenderDir, { recursive: true }); + } + + const packagesDirectory = process.env['DEFENDER_PACKAGES_DIRECTORY'] || path.join(defenderDir, 'packages'); + process.env['DEFENDER_PACKAGES_DIRECTORY'] = packagesDirectory; + + if (!process.env['DEFENDER_FILEPATH']) { + const cliVersion = resolveCliVersion(); + core.debug(`Installing Defender CLI version: ${cliVersion}`); + await installer.install(cliVersion); + } +} + +/** + * Resolves the CLI version to install. + */ +function resolveCliVersion(): string { + let version = process.env['DEFENDER_VERSION'] || 'latest'; + + if (version.includes('*')) { + version = 'Latest'; + } + + core.debug(`Resolved Defender CLI version: ${version}`); + return version; +} + +/** + * Gets the Defender CLI file path from environment. + */ +function getCliFilePath(): string | undefined { + return process.env['DEFENDER_FILEPATH']; +} diff --git a/src/v2/defender-helpers.ts b/src/v2/defender-helpers.ts new file mode 100644 index 00000000..f230586e --- /dev/null +++ b/src/v2/defender-helpers.ts @@ -0,0 +1,215 @@ +import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as os from 'os'; +import { Writable } from 'stream'; + +/** + * Enum for the possible inputs for the task (specified in action.yml) + */ +export enum Inputs { + Command = 'command', + Args = 'args', + FileSystemPath = 'fileSystemPath', + ImageName = 'imageName', + ModelPath = 'modelPath', + Break = 'break', + Debug = 'debug', + PrSummary = 'pr-summary', + Policy = 'policy' +} + +/* + * Enum for the possible scan type values for the Inputs.Command + */ +export enum ScanType { + FileSystem = 'fs', + Image = 'image', + Model = 'model' +} + +/** + * Enum for defining constants used in the task. + */ +export enum Constants { + Unknown = 'unknown', + PreJobStartTime = 'PREJOBSTARTTIME', + DefenderExecutable = 'Defender' +} + +/** + * Validates the scan type input and returns the corresponding enum value. + */ +export function validateScanType(scanTypeInput: string): ScanType { + const scanType = scanTypeInput as ScanType; + if (!Object.values(ScanType).includes(scanType)) { + throw new Error(`Invalid scan type: ${scanTypeInput}. Valid options are: ${Object.values(ScanType).join(', ')}`); + } + return scanType; +} + +/** + * Validates the filesystem path input for filesystem scans. + */ +export function validateFileSystemPath(fsPath: string): string { + if (!fsPath || fsPath.trim() === '') { + throw new Error('Filesystem path cannot be empty for filesystem scan'); + } + + const trimmedPath = fsPath.trim(); + + if (!fs.existsSync(trimmedPath)) { + throw new Error(`Filesystem path does not exist: ${trimmedPath}`); + } + + return trimmedPath; +} + +/** + * Checks if a given string is a URL (http:// or https://). + */ +export function isUrl(input: string): boolean { + if (!input) { + return false; + } + const lowercased = input.toLowerCase(); + return lowercased.startsWith('http://') || lowercased.startsWith('https://'); +} + +/** + * Validates a URL for model scanning. + */ +export function validateModelUrl(url: string): string { + try { + const parsedUrl = new URL(url); + + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new Error(`Invalid URL protocol: ${parsedUrl.protocol}. Only http:// and https:// are supported.`); + } + + if (!parsedUrl.hostname) { + throw new Error('URL must have a valid hostname.'); + } + + return url; + } catch (error) { + if (error instanceof TypeError) { + throw new Error(`Invalid URL format: ${url}`); + } + throw error; + } +} + +/** + * Validates the model path input for AI model scans. + * Supports both local file paths and URLs. + */ +export function validateModelPath(modelPath: string): string { + if (!modelPath || modelPath.trim() === '') { + throw new Error('Model path cannot be empty for model scan'); + } + + const trimmedPath = modelPath.trim(); + + if (isUrl(trimmedPath)) { + return validateModelUrl(trimmedPath); + } + + if (!fs.existsSync(trimmedPath)) { + throw new Error(`Model path does not exist: ${trimmedPath}`); + } + + const stats = fs.statSync(trimmedPath); + if (!stats.isFile() && !stats.isDirectory()) { + throw new Error(`Model path must be a file or directory: ${trimmedPath}`); + } + + return trimmedPath; +} + +/** + * Validates the image name input for container image scans. + */ +export function validateImageName(imageName: string): string { + if (!imageName || imageName.trim() === '') { + throw new Error('Image name cannot be empty for image scan'); + } + + const trimmedImageName = imageName.trim(); + + const imageNameRegex = /^(?:(?:[a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*(?::[0-9]+)?\/)?[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)*)(?::[a-zA-Z0-9._-]+|@sha256:[a-fA-F0-9]{64})?$/; + + if (!imageNameRegex.test(trimmedImageName)) { + throw new Error(`Invalid image name format: ${trimmedImageName}. Image name should follow container image naming conventions.`); + } + + return trimmedImageName; +} + +/** + * Sets up debug logging. When enabled, sets RUNNER_DEBUG to enable verbose logging. + */ +export function setupDebugLogging(enabled: boolean): void { + if (enabled) { + process.env['RUNNER_DEBUG'] = '1'; + core.debug('Debug logging enabled'); + } +} + +/** + * Writes the specified data to the specified output stream, followed by the platform-specific end-of-line character. + */ +export function writeToOutStream(data: string, outStream: Writable = process.stdout): void { + outStream.write(data.trim() + os.EOL); +} + +/** + * Encodes a string to base64. + */ +export const encode = (str: string): string => Buffer.from(str, 'binary').toString('base64'); + +/** + * Returns the encoded content of the Docker version, Docker events, and Docker images. + */ +export function getEncodedContent( + dockerVersion: string, + dockerEvents: string, + dockerImages: string +): string { + let data: string[] = []; + data.push('DockerVersion: ' + dockerVersion); + data.push('DockerEvents:'); + data.push(dockerEvents); + data.push('DockerImages:'); + data.push(dockerImages); + return encode(data.join(os.EOL)); +} + +/** + * Parses additional CLI arguments from a string into an array. + * Handles quoted strings and splits on whitespace. + */ +export function parseAdditionalArgs(additionalArgs: string | undefined): string[] { + if (!additionalArgs || additionalArgs.trim() === '') { + return []; + } + + const args: string[] = []; + const trimmedArgs = additionalArgs.trim(); + + const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g; + const matches = trimmedArgs.match(regex); + + if (matches) { + for (const match of matches) { + let arg = match; + if ((arg.startsWith('"') && arg.endsWith('"')) || + (arg.startsWith("'") && arg.endsWith("'"))) { + arg = arg.slice(1, -1); + } + args.push(arg); + } + } + + core.debug(`Parsed additional arguments: ${JSON.stringify(args)}`); + return args; +} diff --git a/src/v2/defender-installer.ts b/src/v2/defender-installer.ts new file mode 100644 index 00000000..ae6c3321 --- /dev/null +++ b/src/v2/defender-installer.ts @@ -0,0 +1,193 @@ +import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as https from 'https'; +import * as http from 'http'; +import * as path from 'path'; +import * as os from 'os'; + +const downloadBaseUrl = 'https://cli.dfd.security.azure.com/public'; +const maxRetries = 3; +const downloadTimeoutMs = 30000; + +/** + * Installs the Defender CLI if not already present. + * @param cliVersion - The version of the CLI to install (default: 'latest') + */ +export async function install(cliVersion: string = 'latest'): Promise { + // If DEFENDER_FILEPATH is already set and the file exists, skip installation + const existingPath = process.env['DEFENDER_FILEPATH']; + if (existingPath && fs.existsSync(existingPath)) { + core.debug(`Defender CLI already installed at: ${existingPath}`); + return; + } + + // Check if DEFENDER_DIRECTORY is set (pre-installed CLI) + const existingDir = process.env['DEFENDER_DIRECTORY']; + if (existingDir && fs.existsSync(existingDir)) { + const fileName = resolveFileName(); + const filePath = path.join(existingDir, fileName); + if (fs.existsSync(filePath)) { + core.debug(`Found pre-installed Defender CLI at: ${filePath}`); + setVariables(existingDir, fileName, cliVersion); + return; + } + } + + // Determine packages directory + const toolCacheDir = process.env['RUNNER_TOOL_CACHE'] || path.join(os.homedir(), '.defender'); + const packagesDirectory = process.env['DEFENDER_PACKAGES_DIRECTORY'] || path.join(toolCacheDir, '_defender', 'packages'); + + if (!fs.existsSync(packagesDirectory)) { + fs.mkdirSync(packagesDirectory, { recursive: true }); + } + + const fileName = resolveFileName(); + + // Retry download up to maxRetries times + let lastError: Error | undefined; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + core.info(`Downloading Defender CLI (attempt ${attempt}/${maxRetries})...`); + await downloadDefenderCli(packagesDirectory, fileName, cliVersion); + setVariables(packagesDirectory, fileName, cliVersion, true); + core.info(`Defender CLI installed successfully.`); + return; + } catch (error) { + lastError = error as Error; + core.warning(`Download attempt ${attempt} failed: ${lastError.message}`); + if (attempt < maxRetries) { + core.info('Retrying...'); + } + } + } + + throw new Error(`Failed to install Defender CLI after ${maxRetries} attempts: ${lastError?.message}`); +} + +/** + * Downloads the Defender CLI binary. + */ +async function downloadDefenderCli( + packagesDirectory: string, + fileName: string, + cliVersion: string +): Promise { + const versionDir = path.join(packagesDirectory, `defender-cli.${cliVersion}`); + if (!fs.existsSync(versionDir)) { + fs.mkdirSync(versionDir, { recursive: true }); + } + + const filePath = path.join(versionDir, fileName); + const downloadUrl = `${downloadBaseUrl}/${cliVersion.toLowerCase()}/${fileName}`; + + core.debug(`Downloading from: ${downloadUrl}`); + core.debug(`Saving to: ${filePath}`); + + await downloadFile(downloadUrl, filePath); + + // Make executable on non-Windows platforms + if (process.platform !== 'win32') { + fs.chmodSync(filePath, 0o755); + } +} + +/** + * Downloads a file from a URL, following redirects. + */ +function downloadFile(url: string, filePath: string): Promise { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + const request = https.get(url, { timeout: downloadTimeoutMs }, (response) => { + // Follow redirects (301, 302) + if (response.statusCode === 301 || response.statusCode === 302) { + file.close(); + fs.unlinkSync(filePath); + const redirectUrl = response.headers.location; + if (!redirectUrl) { + return reject(new Error('Redirect without location header')); + } + core.debug(`Following redirect to: ${redirectUrl}`); + downloadFile(redirectUrl, filePath).then(resolve).catch(reject); + return; + } + + if (response.statusCode !== 200) { + file.close(); + fs.unlinkSync(filePath); + return reject(new Error(`Download failed with status code: ${response.statusCode}`)); + } + + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }); + + request.on('error', (error) => { + file.close(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + reject(new Error(`Download error: ${error.message}`)); + }); + + request.on('timeout', () => { + request.destroy(); + file.close(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + reject(new Error('Download timed out')); + }); + }); +} + +/** + * Resolves the platform-specific Defender CLI binary filename. + */ +export function resolveFileName(): string { + const platform = os.platform(); + const arch = os.arch(); + + switch (platform) { + case 'win32': + if (arch === 'arm64') return 'Defender_win-arm64.exe'; + if (arch === 'ia32' || arch === 'x32') return 'Defender_win-x86.exe'; + return 'Defender_win-x64.exe'; + case 'linux': + if (arch === 'arm64') return 'Defender_linux-arm64'; + return 'Defender_linux-x64'; + case 'darwin': + if (arch === 'arm64') return 'Defender_osx-arm64'; + return 'Defender_osx-x64'; + default: + core.warning(`Unknown platform: ${platform}. Defaulting to linux-x64.`); + return 'Defender_linux-x64'; + } +} + +/** + * Sets environment variables for the Defender CLI location. + */ +export function setVariables( + packagesDirectory: string, + fileName: string, + cliVersion: string, + validate: boolean = false +): void { + const defenderDir = path.join(packagesDirectory, `defender-cli.${cliVersion}`); + const defenderFilePath = path.join(defenderDir, fileName); + + if (validate && !fs.existsSync(defenderFilePath)) { + throw new Error(`Defender CLI not found after download: ${defenderFilePath}`); + } + + process.env['DEFENDER_DIRECTORY'] = defenderDir; + process.env['DEFENDER_FILEPATH'] = defenderFilePath; + process.env['DEFENDER_INSTALLEDVERSION'] = cliVersion; + + core.debug(`DEFENDER_DIRECTORY=${defenderDir}`); + core.debug(`DEFENDER_FILEPATH=${defenderFilePath}`); + core.debug(`DEFENDER_INSTALLEDVERSION=${cliVersion}`); +} diff --git a/src/v2/defender-interface.ts b/src/v2/defender-interface.ts new file mode 100644 index 00000000..ab9b30f8 --- /dev/null +++ b/src/v2/defender-interface.ts @@ -0,0 +1,26 @@ +/* + * Interface for the MicrosoftDefenderCLI task. + * Mirrors the AzDevOps v2 defender-interface.ts, adapted for GitHub Actions 3-phase lifecycle. + */ +export interface IMicrosoftDefenderCLI { + readonly succeedOnError: boolean; + runPreJob(): any; + runMain(): any; + runPostJob(): any; +} + +/* + * Factory interface for creating IMicrosoftDefenderCLI instances. + */ +export interface IMicrosoftDefenderCLIFactory { + new(): IMicrosoftDefenderCLI; +} + +/** + * Returns an instance of IMicrosoftDefenderCLI based on the input runner. + * @param runner - The factory to use to create the instance. + * @returns An instance of IMicrosoftDefenderCLI. + */ +export function getDefenderExecutor(runner: IMicrosoftDefenderCLIFactory): IMicrosoftDefenderCLI { + return new runner(); +} diff --git a/src/v2/defender-main.ts b/src/v2/defender-main.ts new file mode 100644 index 00000000..ffc51e34 --- /dev/null +++ b/src/v2/defender-main.ts @@ -0,0 +1,34 @@ +import * as core from '@actions/core'; +import { MicrosoftDefenderCLI } from './defender-cli'; +import { IMicrosoftDefenderCLI, IMicrosoftDefenderCLIFactory, getDefenderExecutor } from './defender-interface'; +import { writeToOutStream } from './defender-helpers'; + +let succeedOnError = false; + +/** + * Returns an instance of IMicrosoftDefenderCLI. + * The scan type (fs, image, model) is determined by the CLI class based on action inputs. + */ +function _getDefenderRunner(): IMicrosoftDefenderCLI { + return getDefenderExecutor(MicrosoftDefenderCLI); +} + +/** + * Main entry point for the Defender CLI v2 action. + * Creates and runs the Defender CLI which handles all scan types (filesystem, image, model). + */ +async function run() { + core.debug('Starting Microsoft Defender for DevOps scan'); + const defenderRunner = _getDefenderRunner(); + succeedOnError = defenderRunner.succeedOnError; + await defenderRunner.runMain(); +} + +run().catch(error => { + if (succeedOnError) { + writeToOutStream('Ran into error: ' + error); + core.info('Finished execution with error (succeedOnError=true)'); + } else { + core.setFailed(error); + } +}); diff --git a/src/v2/job-summary.ts b/src/v2/job-summary.ts new file mode 100644 index 00000000..6561346c --- /dev/null +++ b/src/v2/job-summary.ts @@ -0,0 +1,393 @@ +import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * SARIF result level (severity) mappings + */ +export enum SarifLevel { + Error = 'error', + Warning = 'warning', + Note = 'note', + None = 'none' +} + +/** + * Vulnerability severity levels + */ +export enum Severity { + Critical = 'critical', + High = 'high', + Medium = 'medium', + Low = 'low', + Unknown = 'unknown' +} + +/** + * Represents a parsed vulnerability from SARIF + */ +export interface Vulnerability { + ruleId: string; + message: string; + severity: Severity; + location?: string; + cveId?: string; +} + +/** + * Summary statistics for vulnerabilities + */ +export interface VulnerabilitySummary { + total: number; + critical: number; + high: number; + medium: number; + low: number; + unknown: number; + vulnerabilities: Vulnerability[]; +} + +interface SarifLocation { + physicalLocation?: { + artifactLocation?: { + uri?: string; + }; + region?: { + startLine?: number; + }; + }; +} + +interface SarifResult { + ruleId?: string; + message?: { + text?: string; + }; + level?: string; + locations?: SarifLocation[]; + properties?: { + severity?: string; + cveId?: string; + [key: string]: unknown; + }; +} + +interface SarifRule { + id: string; + shortDescription?: { + text?: string; + }; + defaultConfiguration?: { + level?: string; + }; + properties?: { + severity?: string; + [key: string]: unknown; + }; +} + +interface SarifRun { + tool?: { + driver?: { + name?: string; + rules?: SarifRule[]; + }; + }; + results?: SarifResult[]; +} + +interface SarifDocument { + $schema?: string; + version?: string; + runs?: SarifRun[]; +} + +/** + * Maps SARIF level to severity + */ +export function mapLevelToSeverity(level: string | undefined, properties?: { severity?: string }): Severity { + if (properties?.severity) { + const propSeverity = properties.severity.toLowerCase(); + if (propSeverity === 'critical') return Severity.Critical; + if (propSeverity === 'high') return Severity.High; + if (propSeverity === 'medium') return Severity.Medium; + if (propSeverity === 'low') return Severity.Low; + } + + switch (level?.toLowerCase()) { + case SarifLevel.Error: + return Severity.High; + case SarifLevel.Warning: + return Severity.Medium; + case SarifLevel.Note: + return Severity.Low; + case SarifLevel.None: + return Severity.Low; + default: + return Severity.Unknown; + } +} + +/** + * Extracts CVE ID from rule ID or properties + */ +export function extractCveId(ruleId: string | undefined, properties?: { cveId?: string }): string | undefined { + if (properties?.cveId) { + return properties.cveId; + } + + if (ruleId) { + const cveMatch = ruleId.match(/CVE-\d{4}-\d+/i); + if (cveMatch) { + return cveMatch[0].toUpperCase(); + } + } + + return undefined; +} + +/** + * Formats a location from SARIF into a readable string + */ +export function formatLocation(locations?: SarifLocation[]): string | undefined { + if (!locations || locations.length === 0) { + return undefined; + } + + const loc = locations[0]; + const uri = loc.physicalLocation?.artifactLocation?.uri; + const line = loc.physicalLocation?.region?.startLine; + + if (uri) { + return line ? `${uri}:${line}` : uri; + } + + return undefined; +} + +/** + * Parses a SARIF document and extracts vulnerability information + */ +export function parseSarifContent(sarifContent: string): VulnerabilitySummary { + const summary: VulnerabilitySummary = { + total: 0, + critical: 0, + high: 0, + medium: 0, + low: 0, + unknown: 0, + vulnerabilities: [] + }; + + let sarif: SarifDocument; + try { + sarif = JSON.parse(sarifContent) as SarifDocument; + } catch (error) { + core.warning(`Failed to parse SARIF content: ${error}`); + return summary; + } + + if (!sarif.runs || sarif.runs.length === 0) { + core.debug('No runs found in SARIF document'); + return summary; + } + + const rulesMap = new Map(); + + for (const run of sarif.runs) { + if (run.tool?.driver?.rules) { + for (const rule of run.tool.driver.rules) { + rulesMap.set(rule.id, rule); + } + } + + if (run.results) { + for (const result of run.results) { + const ruleId = result.ruleId || 'unknown'; + const rule = rulesMap.get(ruleId); + + const severity = mapLevelToSeverity( + result.level || rule?.defaultConfiguration?.level, + result.properties || rule?.properties + ); + + const vulnerability: Vulnerability = { + ruleId, + message: result.message?.text || rule?.shortDescription?.text || 'No description available', + severity, + location: formatLocation(result.locations), + cveId: extractCveId(ruleId, result.properties) + }; + + summary.vulnerabilities.push(vulnerability); + summary.total++; + + switch (severity) { + case Severity.Critical: + summary.critical++; + break; + case Severity.High: + summary.high++; + break; + case Severity.Medium: + summary.medium++; + break; + case Severity.Low: + summary.low++; + break; + default: + summary.unknown++; + } + } + } + } + + return summary; +} + +/** + * Generates a markdown summary from vulnerability data + */ +export function generateMarkdownSummary( + summary: VulnerabilitySummary, + scanType: string, + target: string, + hasCriticalOrHigh: boolean +): string { + const lines: string[] = []; + + lines.push('# Microsoft Defender for DevOps Scan Results'); + lines.push(''); + + lines.push('## Summary'); + lines.push('| Severity | Count |'); + lines.push('|----------|-------|'); + lines.push(`| 🔴 Critical | ${summary.critical} |`); + lines.push(`| 🟠 High | ${summary.high} |`); + lines.push(`| 🟡 Medium | ${summary.medium} |`); + lines.push(`| 🟢 Low | ${summary.low} |`); + if (summary.unknown > 0) { + lines.push(`| ⚪ Unknown | ${summary.unknown} |`); + } + lines.push(''); + lines.push(`**Total Vulnerabilities**: ${summary.total}`); + lines.push(''); + + if (summary.critical > 0 || summary.high > 0) { + lines.push('## Critical and High Findings'); + + const criticalAndHigh = summary.vulnerabilities.filter( + v => v.severity === Severity.Critical || v.severity === Severity.High + ); + + let index = 1; + for (const vuln of criticalAndHigh.slice(0, 20)) { + const severityIcon = vuln.severity === Severity.Critical ? '🔴' : '🟠'; + const identifier = vuln.cveId || vuln.ruleId; + const location = vuln.location ? ` in \`${vuln.location}\`` : ''; + lines.push(`${index}. ${severityIcon} **${identifier}** - ${vuln.message}${location}`); + index++; + } + + if (criticalAndHigh.length > 20) { + lines.push(`... and ${criticalAndHigh.length - 20} more`); + } + + lines.push(''); + } + + lines.push('## Scan Details'); + lines.push(`- **Scan Type**: ${formatScanType(scanType)}`); + lines.push(`- **Target**: \`${target}\``); + + const statusIcon = hasCriticalOrHigh ? '❌' : '✅'; + const statusText = hasCriticalOrHigh + ? 'Failed (Critical/High vulnerabilities found)' + : 'Passed'; + lines.push(`- **Status**: ${statusIcon} ${statusText}`); + lines.push(''); + + lines.push('---'); + lines.push('*Generated by Microsoft Defender for DevOps*'); + + return lines.join('\n'); +} + +/** + * Formats the scan type for display + */ +function formatScanType(scanType: string): string { + switch (scanType.toLowerCase()) { + case 'fs': + return 'Filesystem'; + case 'image': + return 'Container Image'; + case 'model': + return 'AI Model'; + default: + return scanType; + } +} + +/** + * Creates a no-results summary when no vulnerabilities are found + */ +export function generateNoFindingsSummary(scanType: string, target: string): string { + const lines: string[] = []; + + lines.push('# Microsoft Defender for DevOps Scan Results'); + lines.push(''); + lines.push('## Summary'); + lines.push('✅ **No vulnerabilities found!**'); + lines.push(''); + lines.push('## Scan Details'); + lines.push(`- **Scan Type**: ${formatScanType(scanType)}`); + lines.push(`- **Target**: \`${target}\``); + lines.push('- **Status**: ✅ Passed'); + lines.push(''); + lines.push('---'); + lines.push('*Generated by Microsoft Defender for DevOps*'); + + return lines.join('\n'); +} + +/** + * Posts the vulnerability summary to GitHub Job Summary. + * Reads SARIF output, parses it, generates markdown, and writes to job summary. + */ +export async function postJobSummary( + sarifPath: string, + scanType: string, + target: string +): Promise { + try { + core.debug(`Attempting to post job summary from SARIF: ${sarifPath}`); + + if (!fs.existsSync(sarifPath)) { + core.warning(`SARIF file not found at ${sarifPath}. Skipping job summary.`); + return false; + } + + const sarifContent = fs.readFileSync(sarifPath, 'utf8'); + const summary = parseSarifContent(sarifContent); + + core.debug(`Parsed ${summary.total} vulnerabilities from SARIF`); + + const hasCriticalOrHigh = summary.critical > 0 || summary.high > 0; + + let markdown: string; + if (summary.total === 0) { + markdown = generateNoFindingsSummary(scanType, target); + } else { + markdown = generateMarkdownSummary(summary, scanType, target, hasCriticalOrHigh); + } + + await core.summary.addRaw(markdown).write(); + core.debug('Posted summary to GitHub Job Summary'); + + return true; + } catch (error) { + core.warning(`Failed to post job summary: ${error}`); + return false; + } +} diff --git a/src/v2/post.ts b/src/v2/post.ts new file mode 100644 index 00000000..f2374316 --- /dev/null +++ b/src/v2/post.ts @@ -0,0 +1,11 @@ +import * as core from '@actions/core'; +import { ContainerMapping } from './container-mapping'; +import { getDefenderExecutor } from './defender-interface'; + +async function runPost() { + await getDefenderExecutor(ContainerMapping).runPostJob(); +} + +runPost().catch((error) => { + core.debug(error); +}); diff --git a/src/v2/pre.ts b/src/v2/pre.ts new file mode 100644 index 00000000..de2eb59b --- /dev/null +++ b/src/v2/pre.ts @@ -0,0 +1,11 @@ +import * as core from '@actions/core'; +import { ContainerMapping } from './container-mapping'; +import { getDefenderExecutor } from './defender-interface'; + +async function runPre() { + await getDefenderExecutor(ContainerMapping).runPreJob(); +} + +runPre().catch((error) => { + core.debug(error); +}); diff --git a/test/defender-client.tests.ts b/test/defender-client.tests.ts new file mode 100644 index 00000000..cdb0ca2d --- /dev/null +++ b/test/defender-client.tests.ts @@ -0,0 +1,79 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import * as exec from '@actions/exec'; +import * as core from '@actions/core'; +import * as installer from '../lib/v2/defender-installer'; + +describe('defender-client', () => { + let execStub: sinon.SinonStub; + let installStub: sinon.SinonStub; + + beforeEach(() => { + execStub = sinon.stub(exec, 'exec'); + installStub = sinon.stub(installer, 'install'); + + // Set up environment for tests + process.env['DEFENDER_FILEPATH'] = '/path/to/defender'; + process.env['RUNNER_TOOL_CACHE'] = '/tmp/tool-cache'; + + installStub.resolves(); + execStub.resolves(0); + }); + + afterEach(() => { + execStub.restore(); + installStub.restore(); + delete process.env['DEFENDER_FILEPATH']; + delete process.env['RUNNER_TOOL_CACHE']; + delete process.env['DEFENDER_PACKAGES_DIRECTORY']; + delete process.env['RUNNER_DEBUG']; + }); + + it('should call exec with correct args for filesystem scan', async () => { + const { scanDirectory } = require('../lib/v2/defender-client'); + await scanDirectory('/test/path', 'github', '/output/defender.sarif', [0], []); + + sinon.assert.calledOnce(execStub); + const args = execStub.firstCall.args; + assert.strictEqual(args[0], '/path/to/defender'); + assert.ok(args[1].includes('scan')); + assert.ok(args[1].includes('fs')); + assert.ok(args[1].includes('/test/path')); + assert.ok(args[1].includes('--defender-policy')); + assert.ok(args[1].includes('github')); + assert.ok(args[1].includes('--defender-output')); + }); + + it('should call exec with correct args for image scan', async () => { + const { scanImage } = require('../lib/v2/defender-client'); + await scanImage('nginx:latest', 'mdc', '/output/defender.sarif', [0], ['--defender-break']); + + sinon.assert.calledOnce(execStub); + const args = execStub.firstCall.args; + assert.strictEqual(args[0], '/path/to/defender'); + assert.ok(args[1].includes('scan')); + assert.ok(args[1].includes('image')); + assert.ok(args[1].includes('nginx:latest')); + assert.ok(args[1].includes('--defender-break')); + }); + + it('should throw when CLI exits with non-zero code', async () => { + execStub.resolves(1); + const { scanDirectory } = require('../lib/v2/defender-client'); + + await assert.rejects( + () => scanDirectory('/test/path'), + /error exit code: 1/ + ); + }); + + it('should add --defender-debug when RUNNER_DEBUG is set', async () => { + process.env['RUNNER_DEBUG'] = '1'; + const { scanDirectory } = require('../lib/v2/defender-client'); + await scanDirectory('/test/path', 'github', '/output/defender.sarif', [0], []); + + sinon.assert.calledOnce(execStub); + const args = execStub.firstCall.args[1]; + assert.ok(args.includes('--defender-debug')); + }); +}); diff --git a/test/defender-helpers.tests.ts b/test/defender-helpers.tests.ts new file mode 100644 index 00000000..b2639920 --- /dev/null +++ b/test/defender-helpers.tests.ts @@ -0,0 +1,180 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + validateScanType, + validateFileSystemPath, + validateImageName, + validateModelPath, + validateModelUrl, + isUrl, + parseAdditionalArgs, + ScanType +} from '../lib/v2/defender-helpers'; + +describe('defender-helpers', () => { + + describe('validateScanType', () => { + it('should accept "fs" as a valid scan type', () => { + assert.strictEqual(validateScanType('fs'), ScanType.FileSystem); + }); + + it('should accept "image" as a valid scan type', () => { + assert.strictEqual(validateScanType('image'), ScanType.Image); + }); + + it('should accept "model" as a valid scan type', () => { + assert.strictEqual(validateScanType('model'), ScanType.Model); + }); + + it('should throw for invalid scan type', () => { + assert.throws(() => validateScanType('invalid'), /Invalid scan type/); + }); + + it('should throw for empty string', () => { + assert.throws(() => validateScanType(''), /Invalid scan type/); + }); + }); + + describe('validateFileSystemPath', () => { + it('should return trimmed path when it exists', () => { + // Use __dirname as a known-existing path + const result = validateFileSystemPath(` ${__dirname} `); + assert.strictEqual(result, __dirname); + }); + + it('should throw when path is empty', () => { + assert.throws(() => validateFileSystemPath(''), /cannot be empty/); + }); + + it('should throw when path is whitespace', () => { + assert.throws(() => validateFileSystemPath(' '), /cannot be empty/); + }); + + it('should throw when path does not exist', () => { + assert.throws(() => validateFileSystemPath('/definitely/nonexistent/path/abc123'), /does not exist/); + }); + }); + + describe('validateImageName', () => { + it('should accept simple image name', () => { + assert.strictEqual(validateImageName('nginx'), 'nginx'); + }); + + it('should accept image with tag', () => { + assert.strictEqual(validateImageName('nginx:latest'), 'nginx:latest'); + }); + + it('should accept fully qualified image name', () => { + assert.strictEqual( + validateImageName('myregistry.azurecr.io/myapp:v1.0'), + 'myregistry.azurecr.io/myapp:v1.0' + ); + }); + + it('should accept image with sha256 digest', () => { + const digest = 'nginx@sha256:' + 'a'.repeat(64); + assert.strictEqual(validateImageName(digest), digest); + }); + + it('should throw for empty image name', () => { + assert.throws(() => validateImageName(''), /cannot be empty/); + }); + + it('should trim whitespace', () => { + assert.strictEqual(validateImageName(' nginx:latest '), 'nginx:latest'); + }); + }); + + describe('isUrl', () => { + it('should return true for http URL', () => { + assert.strictEqual(isUrl('http://example.com'), true); + }); + + it('should return true for https URL', () => { + assert.strictEqual(isUrl('https://example.com/model'), true); + }); + + it('should return false for local path', () => { + assert.strictEqual(isUrl('/local/path'), false); + }); + + it('should return false for empty string', () => { + assert.strictEqual(isUrl(''), false); + }); + + it('should return false for null/undefined', () => { + assert.strictEqual(isUrl(null as any), false); + assert.strictEqual(isUrl(undefined as any), false); + }); + }); + + describe('validateModelUrl', () => { + it('should accept valid https URL', () => { + assert.strictEqual(validateModelUrl('https://example.com/model'), 'https://example.com/model'); + }); + + it('should accept valid http URL', () => { + assert.strictEqual(validateModelUrl('http://example.com/model'), 'http://example.com/model'); + }); + + it('should throw for invalid URL format', () => { + assert.throws(() => validateModelUrl('not-a-url'), /Invalid URL/); + }); + }); + + describe('validateModelPath', () => { + it('should throw for empty path', () => { + assert.throws(() => validateModelPath(''), /cannot be empty/); + }); + + it('should accept URL without checking filesystem', () => { + const result = validateModelPath('https://example.com/model'); + assert.strictEqual(result, 'https://example.com/model'); + }); + + it('should accept existing directory as model path', () => { + // Use __dirname as a known-existing directory + const result = validateModelPath(__dirname); + assert.strictEqual(result, __dirname); + }); + + it('should throw when local path does not exist', () => { + assert.throws(() => validateModelPath('/definitely/nonexistent/model/path'), /does not exist/); + }); + }); + + describe('parseAdditionalArgs', () => { + it('should return empty array for undefined', () => { + assert.deepStrictEqual(parseAdditionalArgs(undefined), []); + }); + + it('should return empty array for empty string', () => { + assert.deepStrictEqual(parseAdditionalArgs(''), []); + }); + + it('should return empty array for whitespace', () => { + assert.deepStrictEqual(parseAdditionalArgs(' '), []); + }); + + it('should parse simple arguments', () => { + assert.deepStrictEqual(parseAdditionalArgs('--flag1 --flag2'), ['--flag1', '--flag2']); + }); + + it('should handle quoted arguments', () => { + assert.deepStrictEqual( + parseAdditionalArgs('--flag "value with spaces"'), + ['--flag', 'value with spaces'] + ); + }); + + it('should handle single-quoted arguments', () => { + assert.deepStrictEqual( + parseAdditionalArgs("--flag 'value with spaces'"), + ['--flag', 'value with spaces'] + ); + }); + }); +}); diff --git a/test/defender-installer.tests.ts b/test/defender-installer.tests.ts new file mode 100644 index 00000000..c52fb368 --- /dev/null +++ b/test/defender-installer.tests.ts @@ -0,0 +1,76 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { resolveFileName, setVariables } from '../lib/v2/defender-installer'; + +describe('defender-installer', () => { + + describe('resolveFileName', () => { + it('should return a platform-appropriate binary name', () => { + const result = resolveFileName(); + const platform = process.platform; + + if (platform === 'win32') { + assert.ok(result.startsWith('Defender_win-'), `Expected Windows binary, got: ${result}`); + assert.ok(result.endsWith('.exe'), `Expected .exe extension, got: ${result}`); + } else if (platform === 'linux') { + assert.ok(result.startsWith('Defender_linux-'), `Expected Linux binary, got: ${result}`); + assert.ok(!result.endsWith('.exe'), `Unexpected .exe extension on Linux`); + } else if (platform === 'darwin') { + assert.ok(result.startsWith('Defender_osx-'), `Expected macOS binary, got: ${result}`); + assert.ok(!result.endsWith('.exe'), `Unexpected .exe extension on macOS`); + } + }); + + it('should include architecture in the filename', () => { + const result = resolveFileName(); + assert.ok( + result.includes('x64') || result.includes('arm64') || result.includes('x86'), + `Expected architecture in filename, got: ${result}` + ); + }); + + it('should return a non-empty string', () => { + const result = resolveFileName(); + assert.ok(result.length > 0); + }); + }); + + describe('setVariables', () => { + beforeEach(() => { + delete process.env['DEFENDER_DIRECTORY']; + delete process.env['DEFENDER_FILEPATH']; + delete process.env['DEFENDER_INSTALLEDVERSION']; + }); + + afterEach(() => { + delete process.env['DEFENDER_DIRECTORY']; + delete process.env['DEFENDER_FILEPATH']; + delete process.env['DEFENDER_INSTALLEDVERSION']; + }); + + it('should set environment variables correctly', () => { + const packagesDir = path.join(os.tmpdir(), 'test-packages'); + setVariables(packagesDir, 'Defender_linux-x64', 'latest'); + + assert.ok(process.env['DEFENDER_DIRECTORY']?.includes('test-packages')); + assert.ok(process.env['DEFENDER_FILEPATH']?.includes('Defender_linux-x64')); + assert.strictEqual(process.env['DEFENDER_INSTALLEDVERSION'], 'latest'); + }); + + it('should throw when validate=true and file does not exist', () => { + const packagesDir = path.join(os.tmpdir(), 'nonexistent-test-packages'); + assert.throws( + () => setVariables(packagesDir, 'Defender_linux-x64', 'latest', true), + /not found after download/ + ); + }); + + it('should not throw when validate=false and file does not exist', () => { + const packagesDir = path.join(os.tmpdir(), 'nonexistent-test-packages'); + assert.doesNotThrow(() => setVariables(packagesDir, 'Defender_linux-x64', 'latest', false)); + }); + }); +}); diff --git a/test/job-summary.tests.ts b/test/job-summary.tests.ts new file mode 100644 index 00000000..d802d39a --- /dev/null +++ b/test/job-summary.tests.ts @@ -0,0 +1,230 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import * as core from '@actions/core'; +import { + mapLevelToSeverity, + extractCveId, + formatLocation, + parseSarifContent, + generateMarkdownSummary, + generateNoFindingsSummary, + Severity, + SarifLevel +} from '../lib/v2/job-summary'; + +describe('job-summary', () => { + + describe('mapLevelToSeverity', () => { + it('should use properties.severity when available', () => { + assert.strictEqual(mapLevelToSeverity('error', { severity: 'critical' }), Severity.Critical); + }); + + it('should use properties.severity over level', () => { + assert.strictEqual(mapLevelToSeverity('note', { severity: 'high' }), Severity.High); + }); + + it('should map error level to High', () => { + assert.strictEqual(mapLevelToSeverity('error'), Severity.High); + }); + + it('should map warning level to Medium', () => { + assert.strictEqual(mapLevelToSeverity('warning'), Severity.Medium); + }); + + it('should map note level to Low', () => { + assert.strictEqual(mapLevelToSeverity('note'), Severity.Low); + }); + + it('should map none level to Low', () => { + assert.strictEqual(mapLevelToSeverity('none'), Severity.Low); + }); + + it('should return Unknown for undefined level', () => { + assert.strictEqual(mapLevelToSeverity(undefined), Severity.Unknown); + }); + + it('should return Unknown for unrecognized level', () => { + assert.strictEqual(mapLevelToSeverity('unknown-level'), Severity.Unknown); + }); + }); + + describe('extractCveId', () => { + it('should extract CVE from properties', () => { + assert.strictEqual(extractCveId('rule1', { cveId: 'CVE-2024-1234' }), 'CVE-2024-1234'); + }); + + it('should extract CVE from ruleId', () => { + assert.strictEqual(extractCveId('CVE-2024-1234'), 'CVE-2024-1234'); + }); + + it('should extract CVE from mixed case ruleId', () => { + assert.strictEqual(extractCveId('cve-2024-5678'), 'CVE-2024-5678'); + }); + + it('should return undefined when no CVE found', () => { + assert.strictEqual(extractCveId('rule1'), undefined); + }); + + it('should return undefined for undefined inputs', () => { + assert.strictEqual(extractCveId(undefined), undefined); + }); + }); + + describe('formatLocation', () => { + it('should format location with uri and line', () => { + const locations = [{ + physicalLocation: { + artifactLocation: { uri: 'src/main.ts' }, + region: { startLine: 42 } + } + }]; + assert.strictEqual(formatLocation(locations), 'src/main.ts:42'); + }); + + it('should format location with uri only', () => { + const locations = [{ + physicalLocation: { + artifactLocation: { uri: 'src/main.ts' } + } + }]; + assert.strictEqual(formatLocation(locations), 'src/main.ts'); + }); + + it('should return undefined for empty locations', () => { + assert.strictEqual(formatLocation([]), undefined); + }); + + it('should return undefined for undefined locations', () => { + assert.strictEqual(formatLocation(undefined), undefined); + }); + }); + + describe('parseSarifContent', () => { + it('should parse valid SARIF with vulnerabilities', () => { + const sarif = { + version: '2.1.0', + runs: [{ + tool: { + driver: { + name: 'Defender', + rules: [{ + id: 'CVE-2024-1234', + shortDescription: { text: 'Test vulnerability' }, + defaultConfiguration: { level: 'error' } + }] + } + }, + results: [{ + ruleId: 'CVE-2024-1234', + message: { text: 'Found vulnerability' }, + level: 'error', + properties: { severity: 'critical' } + }] + }] + }; + + const summary = parseSarifContent(JSON.stringify(sarif)); + assert.strictEqual(summary.total, 1); + assert.strictEqual(summary.critical, 1); + assert.strictEqual(summary.vulnerabilities[0].ruleId, 'CVE-2024-1234'); + }); + + it('should return empty summary for empty SARIF', () => { + const sarif = { version: '2.1.0', runs: [{ results: [] }] }; + const summary = parseSarifContent(JSON.stringify(sarif)); + assert.strictEqual(summary.total, 0); + }); + + it('should handle invalid JSON gracefully', () => { + const summary = parseSarifContent('not valid json'); + assert.strictEqual(summary.total, 0); + }); + + it('should handle SARIF with no runs', () => { + const summary = parseSarifContent(JSON.stringify({ version: '2.1.0' })); + assert.strictEqual(summary.total, 0); + }); + + it('should count multiple severity levels correctly', () => { + const sarif = { + version: '2.1.0', + runs: [{ + tool: { driver: { name: 'Defender' } }, + results: [ + { ruleId: 'r1', level: 'error', message: { text: 'high' }, properties: { severity: 'high' } }, + { ruleId: 'r2', level: 'warning', message: { text: 'medium' } }, + { ruleId: 'r3', level: 'note', message: { text: 'low' } }, + { ruleId: 'r4', level: 'error', message: { text: 'critical' }, properties: { severity: 'critical' } } + ] + }] + }; + + const summary = parseSarifContent(JSON.stringify(sarif)); + assert.strictEqual(summary.total, 4); + assert.strictEqual(summary.critical, 1); + assert.strictEqual(summary.high, 1); + assert.strictEqual(summary.medium, 1); + assert.strictEqual(summary.low, 1); + }); + }); + + describe('generateMarkdownSummary', () => { + it('should generate summary with critical findings', () => { + const summary = { + total: 2, + critical: 1, + high: 1, + medium: 0, + low: 0, + unknown: 0, + vulnerabilities: [ + { ruleId: 'CVE-2024-1', message: 'Critical issue', severity: Severity.Critical, cveId: 'CVE-2024-1' }, + { ruleId: 'CVE-2024-2', message: 'High issue', severity: Severity.High, cveId: 'CVE-2024-2' } + ] + }; + + const md = generateMarkdownSummary(summary, 'fs', '/src', true); + assert.ok(md.includes('Microsoft Defender')); + assert.ok(md.includes('Critical')); + assert.ok(md.includes('CVE-2024-1')); + assert.ok(md.includes('❌')); + }); + + it('should show passing status when no critical/high findings', () => { + const summary = { + total: 1, + critical: 0, + high: 0, + medium: 1, + low: 0, + unknown: 0, + vulnerabilities: [ + { ruleId: 'r1', message: 'Medium issue', severity: Severity.Medium } + ] + }; + + const md = generateMarkdownSummary(summary, 'image', 'nginx:latest', false); + assert.ok(md.includes('✅')); + assert.ok(md.includes('Passed')); + }); + }); + + describe('generateNoFindingsSummary', () => { + it('should generate clean scan summary', () => { + const md = generateNoFindingsSummary('fs', '/src'); + assert.ok(md.includes('No vulnerabilities found')); + assert.ok(md.includes('Filesystem')); + assert.ok(md.includes('✅')); + }); + + it('should format image scan type correctly', () => { + const md = generateNoFindingsSummary('image', 'nginx:latest'); + assert.ok(md.includes('Container Image')); + }); + + it('should format model scan type correctly', () => { + const md = generateNoFindingsSummary('model', '/models/test.onnx'); + assert.ok(md.includes('AI Model')); + }); + }); +}); diff --git a/test/post.tests.ts b/test/post.tests.ts index 8464a9f6..deb98821 100644 --- a/test/post.tests.ts +++ b/test/post.tests.ts @@ -3,7 +3,7 @@ import https from 'https'; import sinon from 'sinon'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; -import { run, sendReport, _sendReport } from '../lib/post'; +import { run, sendReport, _sendReport } from '../lib/v1/post'; describe('postjob run', function() { let execStub: sinon.SinonStub; diff --git a/test/pre.tests.ts b/test/pre.tests.ts index 5bd2d553..8a8f00ae 100644 --- a/test/pre.tests.ts +++ b/test/pre.tests.ts @@ -1,6 +1,6 @@ import sinon from 'sinon'; import * as core from '@actions/core'; -import { run } from '../lib/pre'; +import { run } from '../lib/v1/pre'; describe('prejob run', () => { let saveStateStub: sinon.SinonStub; From 0ebbec9b1589e127ac83f30110ce0c03965e1a2e Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 11:01:37 +0200 Subject: [PATCH 03/17] chore: update validation workflow policy to azuredevops Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 05a0789a..6d878826 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -23,7 +23,7 @@ jobs: with: command: 'image' imageName: 'ubuntu:latest' - policy: 'github' + policy: 'azuredevops' break: 'false' debug: 'true' pr-summary: 'true' From f941645485d7fc2b88fd908c04ad7946c62b4cf6 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 11:30:48 +0200 Subject: [PATCH 04/17] chore: remove debug flag from validation workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 6d878826..b0c573c0 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -23,9 +23,8 @@ jobs: with: command: 'image' imageName: 'ubuntu:latest' - policy: 'azuredevops' + policy: 'microsoft' break: 'false' - debug: 'true' pr-summary: 'true' # Upload results to the Security tab From b3cc53f5c306ec45c860d06be2c79a26a754e9b2 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 13:41:45 +0200 Subject: [PATCH 05/17] chore: change validation policy to mdc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index b0c573c0..0de349da 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -23,7 +23,7 @@ jobs: with: command: 'image' imageName: 'ubuntu:latest' - policy: 'microsoft' + policy: 'mdc' break: 'false' pr-summary: 'true' From 112711643b1143fb3dfbf4d54765a546f1cb82c6 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 13:48:00 +0200 Subject: [PATCH 06/17] fix: set sarifFile output for downstream SARIF upload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/v2/defender-cli.js | 3 + node_modules/.package-lock.json | 2663 ++++++++----------------------- src/v2/defender-cli.ts | 5 + 3 files changed, 690 insertions(+), 1981 deletions(-) diff --git a/lib/v2/defender-cli.js b/lib/v2/defender-cli.js index 9d9c1084..7354a0d6 100644 --- a/lib/v2/defender-cli.js +++ b/lib/v2/defender-cli.js @@ -129,6 +129,9 @@ class MicrosoftDefenderCLI { } process.env['Defender_Extension'] = 'true'; core.debug('Environment variable set: Defender_Extension=true'); + core.setOutput('sarifFile', outputPath); + core.exportVariable('DEFENDER_SARIF_FILE', outputPath); + core.debug(`sarifFile output set to: ${outputPath}`); try { switch (scanType) { case defender_helpers_1.ScanType.FileSystem: diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 3d2207b8..b74d7bec 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -30,6 +30,102 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@microsoft/security-devops-actions-toolkit": { "version": "1.11.0", "resolved": "https://npm.pkg.github.com/download/@microsoft/security-devops-actions-toolkit/1.11.0/04fef883382f5a7c9b9ac2015dcc419009e2a858", @@ -74,6 +170,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "dev": true, @@ -208,118 +314,6 @@ "normalize-path": "^2.1.1" } }, - "node_modules/anymatch/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/micromatch": { - "version": "3.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/anymatch/node_modules/normalize-path": { "version": "2.1.1", "dev": true, @@ -471,14 +465,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-unique": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assign-symbols": { "version": "1.0.0", "dev": true, @@ -523,17 +509,6 @@ "node": ">= 0.10" } }, - "node_modules/atob": { - "version": "2.1.2", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/bach": { "version": "1.2.0", "dev": true, @@ -558,77 +533,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base": { - "version": "0.11.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/binary-extensions": { "version": "1.13.1", "dev": true, @@ -638,7 +542,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -647,23 +553,16 @@ } }, "node_modules/braces": { - "version": "2.3.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/browser-stdout": { @@ -687,25 +586,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cache-base": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/call-bind": { "version": "1.0.2", "dev": true, @@ -726,18 +606,6 @@ "node": ">=0.10.0" } }, - "node_modules/chalk": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chokidar": { "version": "2.1.8", "dev": true, @@ -779,20 +647,6 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils": { - "version": "0.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-stack": { "version": "4.2.0", "dev": true, @@ -869,18 +723,6 @@ "node": ">=0.10.0" } }, - "node_modules/collection-visit": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -905,11 +747,6 @@ "color-support": "bin.js" } }, - "node_modules/component-emitter": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -934,14 +771,6 @@ "dev": true, "license": "MIT" }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/copy-props": { "version": "2.0.5", "dev": true, @@ -956,6 +785,35 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/d": { "version": "1.0.1", "dev": true, @@ -965,14 +823,6 @@ "type": "^1.0.1" } }, - "node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, "node_modules/decamelize": { "version": "1.2.0", "dev": true, @@ -981,14 +831,6 @@ "node": ">=0.10.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/decompress-response": { "version": "8.1.0", "license": "MIT", @@ -1050,17 +892,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/del": { "version": "7.1.0", "dev": true, @@ -1091,7 +922,9 @@ } }, "node_modules/diff": { - "version": "3.5.0", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.1.tgz", + "integrity": "sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1140,10 +973,17 @@ "node": ">=0.10.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -1162,13 +1002,16 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, "license": "ISC", "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -1206,9 +1049,10 @@ } }, "node_modules/escalade": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -1224,21 +1068,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expand-brackets": { - "version": "2.1.4", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" } }, "node_modules/expand-tilde": { @@ -1270,89 +1131,6 @@ "dev": true, "license": "MIT" }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fancy-log": { "version": "1.3.3", "dev": true, @@ -1396,17 +1174,16 @@ } }, "node_modules/fill-range": { - "version": "4.0.0", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/find-up": { @@ -1435,118 +1212,6 @@ "node": ">= 0.10" } }, - "node_modules/findup-sync/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/micromatch": { - "version": "3.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fined": { "version": "1.2.0", "dev": true, @@ -1617,15 +1282,20 @@ "node": ">=0.10.0" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "MIT", "dependencies": { - "map-cache": "^0.2.2" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fs-mkdirp-stream": { @@ -1896,22 +1566,6 @@ "node": ">= 0.10" } }, - "node_modules/gulp-shell": { - "version": "0.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^3.0.0", - "fancy-log": "^1.3.3", - "lodash.template": "^4.5.0", - "plugin-error": "^1.0.1", - "through2": "^3.0.1", - "tslib": "^1.10.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/gulp-typescript": { "version": "6.0.0-alpha.1", "dev": true, @@ -2002,63 +1656,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", + "node_modules/he": { + "version": "1.2.0", "dev": true, "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "bin": { + "he": "bin/he" } }, - "node_modules/has-values": { - "version": "1.0.0", + "node_modules/homedir-polyfill": { + "version": "1.0.3", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "parse-passwd": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", + "node_modules/hosted-git-info": { + "version": "2.8.9", "dev": true, "license": "ISC" }, @@ -2128,28 +1746,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "dev": true, @@ -2182,49 +1778,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -2263,28 +1816,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-path-cwd": { "version": "3.0.0", "dev": true, @@ -2395,8 +1926,25 @@ "node": ">=0.10.0" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2533,12 +2081,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -2547,23 +2092,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.template": { - "version": "4.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, "node_modules/log-symbols": { "version": "4.1.0", "dev": true, @@ -2599,6 +2127,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/make-iterator": { "version": "1.0.1", "dev": true, @@ -2626,17 +2160,6 @@ "node": ">=0.10.0" } }, - "node_modules/map-visit": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matchdep": { "version": "2.0.0", "dev": true, @@ -2651,30 +2174,6 @@ "node": ">= 0.10.0" } }, - "node_modules/matchdep/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/matchdep/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matchdep/node_modules/findup-sync": { "version": "2.0.0", "dev": true, @@ -2689,374 +2188,181 @@ "node": ">= 0.10" } }, - "node_modules/matchdep/node_modules/is-accessor-descriptor": { - "version": "1.0.0", + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", "dev": true, "license": "MIT", "dependencies": { - "kind-of": "^6.0.0" + "is-extglob": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/matchdep/node_modules/is-data-descriptor": { - "version": "1.0.0", + "node_modules/merge2": { + "version": "1.4.1", "dev": true, "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/matchdep/node_modules/is-descriptor": { - "version": "1.0.2", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.6" } }, - "node_modules/matchdep/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, + "node_modules/mimic-response": { + "version": "4.0.0", "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/matchdep/node_modules/is-glob": { - "version": "3.1.0", + "node_modules/minimatch": { + "version": "3.1.2", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-extglob": "^2.1.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/matchdep/node_modules/is-plain-object": { - "version": "2.0.4", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/matchdep/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/matchdep/node_modules/micromatch": { - "version": "3.1.10", + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/merge2": { - "version": "1.4.1", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/micromatch": { - "version": "4.0.5", + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=8.6" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/micromatch/node_modules/braces": { - "version": "3.0.2", + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/micromatch/node_modules/fill-range": { - "version": "7.0.1", + "node_modules/mocha/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">=8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/micromatch/node_modules/is-number": { + "node_modules/mocha/node_modules/diff": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=0.3.1" } }, - "node_modules/micromatch/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/mimic-response": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha": { - "version": "10.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mocha/node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", "dev": true, "license": "MIT", "engines": { @@ -3066,17 +2372,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", "dev": true, @@ -3094,86 +2389,65 @@ }, "node_modules/mocha/node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/mocha/node_modules/is-number": { - "version": "7.0.0", + "node_modules/mocha/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=8" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/ms": { @@ -3190,20 +2464,23 @@ } }, "node_modules/mocha/node_modules/readdirp": { - "version": "3.6.0", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3215,8 +2492,9 @@ }, "node_modules/mocha/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3225,211 +2503,78 @@ } }, "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/mute-stdout": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/nanoid": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-accessor-descriptor": { - "version": "1.0.0", + "version": "8.1.1", "dev": true, "license": "MIT", "dependencies": { - "kind-of": "^6.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/nanomatch/node_modules/is-data-descriptor": { - "version": "1.0.0", + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { - "kind-of": "^6.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/nanomatch/node_modules/is-descriptor": { - "version": "1.0.2", + "node_modules/mocha/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, - "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/nanomatch/node_modules/is-plain-object": { - "version": "2.0.4", + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", + "node_modules/mute-stdout": { + "version": "1.0.1", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/next-tick": { @@ -3504,30 +2649,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-keys": { "version": "1.1.1", "dev": true, @@ -3536,17 +2657,6 @@ "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.4", "dev": true, @@ -3682,6 +2792,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parse-filepath": { "version": "1.0.2", "dev": true, @@ -3722,14 +2838,6 @@ "node": ">=0.10.0" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-dirname": { "version": "1.0.2", "dev": true, @@ -3754,6 +2862,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "dev": true, @@ -3778,8 +2895,26 @@ "node": ">=0.10.0" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { - "version": "1.8.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3799,6 +2934,12 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "dev": true, @@ -3885,14 +3026,6 @@ "node": ">=0.10.0" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pretty-hrtime": { "version": "1.0.3", "dev": true, @@ -3942,247 +3075,91 @@ "url": "https://feross.org/support" } ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/read-pkg": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/readdirp/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/readdirp/node_modules/kind-of": { - "version": "6.0.3", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "safe-buffer": "^5.1.0" } }, - "node_modules/readdirp/node_modules/micromatch": { - "version": "3.1.10", + "node_modules/read-pkg": { + "version": "1.1.0", "dev": true, "license": "MIT", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/rechoir": { - "version": "0.6.2", + "node_modules/read-pkg-up": { + "version": "1.0.1", "dev": true, + "license": "MIT", "dependencies": { - "resolve": "^1.1.6" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/regex-not": { - "version": "1.0.2", + "node_modules/read-pkg/node_modules/path-type": { + "version": "1.1.0", "dev": true, "license": "MIT", "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", + "node_modules/readable-stream": { + "version": "2.3.8", "dev": true, "license": "MIT", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", + "node_modules/readdirp": { + "version": "2.2.1", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10" } }, - "node_modules/regex-not/node_modules/is-plain-object": { - "version": "2.0.4", + "node_modules/rechoir": { + "version": "0.6.2", "dev": true, - "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "resolve": "^1.1.6" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/remove-bom-buffer": { @@ -4224,22 +3201,6 @@ "dev": true, "license": "ISC" }, - "node_modules/repeat-element": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/replace-ext": { "version": "1.0.1", "dev": true, @@ -4313,19 +3274,6 @@ "node": ">= 0.10" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ret": { - "version": "0.1.15", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -4376,14 +3324,6 @@ "dev": true, "license": "MIT" }, - "node_modules/safe-regex": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/samsam": { "version": "1.3.0", "dev": true, @@ -4409,7 +3349,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4421,29 +3363,37 @@ "dev": true, "license": "ISC" }, - "node_modules/set-value": { - "version": "2.0.1", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/set-value/node_modules/is-plain-object": { - "version": "2.0.4", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/sinon": { @@ -4491,121 +3441,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "dev": true, - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.7.4", "dev": true, @@ -4614,23 +3449,6 @@ "node": ">= 8" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "dev": true, - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, "node_modules/sparkles": { "version": "1.0.1", "dev": true, @@ -4667,69 +3485,12 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/split-string": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "dev": true, "license": "MIT", "engines": { - "node": "*" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "node": "*" } }, "node_modules/stream-exhaust": { @@ -4763,6 +3524,51 @@ "node": ">=0.10.0" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "3.0.1", "dev": true, @@ -4774,6 +3580,28 @@ "node": ">=0.10.0" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "2.0.0", "dev": true, @@ -4874,141 +3702,27 @@ "node": ">=0.10.0" } }, - "node_modules/to-object-path": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-descriptor": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-plain-object": { - "version": "2.0.4", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0" } }, - "node_modules/to-regex/node_modules/kind-of": { - "version": "6.0.3", + "node_modules/to-regex-range/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" } }, "node_modules/to-through": { @@ -5031,11 +3745,6 @@ "xtend": "~4.0.1" } }, - "node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, "node_modules/tunnel": { "version": "0.0.6", "license": "MIT", @@ -5109,20 +3818,6 @@ "node": ">= 0.10" } }, - "node_modules/union-value": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/unique-stream": { "version": "2.3.1", "dev": true, @@ -5132,50 +3827,6 @@ "through2-filter": "^3.0.0" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/upath": { "version": "1.2.0", "dev": true, @@ -5185,19 +3836,6 @@ "yarn": "*" } }, - "node_modules/urix": { - "version": "0.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/use": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -5335,9 +3973,10 @@ "license": "ISC" }, "node_modules/workerpool": { - "version": "6.2.1", - "dev": true, - "license": "Apache-2.0" + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true }, "node_modules/wrap-ansi": { "version": "2.1.0", @@ -5351,6 +3990,68 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, diff --git a/src/v2/defender-cli.ts b/src/v2/defender-cli.ts index 63ebc642..66dda30b 100644 --- a/src/v2/defender-cli.ts +++ b/src/v2/defender-cli.ts @@ -135,6 +135,11 @@ export class MicrosoftDefenderCLI implements IMicrosoftDefenderCLI { process.env['Defender_Extension'] = 'true'; core.debug('Environment variable set: Defender_Extension=true'); + // Set the sarifFile output so downstream steps can reference it + core.setOutput('sarifFile', outputPath); + core.exportVariable('DEFENDER_SARIF_FILE', outputPath); + core.debug(`sarifFile output set to: ${outputPath}`); + try { switch (scanType) { case ScanType.FileSystem: From 96bd7379e2d47d8e3fee61ff10e7ea0ca60a2024 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 14:34:53 +0200 Subject: [PATCH 07/17] chore: add model scan job for Qwen3.5-35B-A3B validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 0de349da..f0c3d0fb 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -33,3 +33,31 @@ jobs: if: always() with: sarif_file: ${{ steps.defender.outputs.sarifFile }} + + defender-model-scan: + name: Defender CLI v2 - Model Scan + + runs-on: self-hosted + + steps: + + # Checkout your code repository to scan + - uses: actions/checkout@v6 + + # Run Defender CLI v2 model scan + - name: Run Defender CLI - Model Scan + uses: ./ + id: defender + with: + command: 'model' + modelPath: 'https://huggingface.co/Qwen/Qwen3.5-35B-A3B' + policy: 'mdc' + break: 'false' + pr-summary: 'true' + + # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} From d38c90d2dec1f636c815b16b70979024f58e36b7 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 14:56:13 +0200 Subject: [PATCH 08/17] fix: call setupEnvironment in model scan to install Defender CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/v2/defender-cli.js | 1 + lib/v2/defender-client.js | 3 ++- src/v2/defender-cli.ts | 4 +++- src/v2/defender-client.ts | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/v2/defender-cli.js b/lib/v2/defender-cli.js index 7354a0d6..6ffe6ad6 100644 --- a/lib/v2/defender-cli.js +++ b/lib/v2/defender-cli.js @@ -165,6 +165,7 @@ class MicrosoftDefenderCLI { } runModelScan(modelPath, policy, outputPath, successfulExitCodes, additionalArgs) { return __awaiter(this, void 0, void 0, function* () { + yield (0, defender_client_1.setupEnvironment)(); const cliFilePath = process.env['DEFENDER_FILEPATH']; if (!cliFilePath) { throw new Error('DEFENDER_FILEPATH environment variable is not set. Defender CLI may not be installed.'); diff --git a/lib/v2/defender-client.js b/lib/v2/defender-client.js index b2d6e608..f95adf70 100644 --- a/lib/v2/defender-client.js +++ b/lib/v2/defender-client.js @@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.scanImage = exports.scanDirectory = void 0; +exports.setupEnvironment = exports.scanImage = exports.scanDirectory = void 0; const core = __importStar(require("@actions/core")); const exec = __importStar(require("@actions/exec")); const fs = __importStar(require("fs")); @@ -108,6 +108,7 @@ function setupEnvironment() { } }); } +exports.setupEnvironment = setupEnvironment; function resolveCliVersion() { let version = process.env['DEFENDER_VERSION'] || 'latest'; if (version.includes('*')) { diff --git a/src/v2/defender-cli.ts b/src/v2/defender-cli.ts index 66dda30b..e6e4c0e2 100644 --- a/src/v2/defender-cli.ts +++ b/src/v2/defender-cli.ts @@ -3,7 +3,7 @@ import * as exec from '@actions/exec'; import * as path from 'path'; import { ScanType, Inputs, validateScanType, validateImageName, validateModelPath, validateFileSystemPath, parseAdditionalArgs, setupDebugLogging } from './defender-helpers'; import { IMicrosoftDefenderCLI } from './defender-interface'; -import { scanDirectory, scanImage } from './defender-client'; +import { scanDirectory, scanImage, setupEnvironment } from './defender-client'; import { postJobSummary } from './job-summary'; /* @@ -185,6 +185,8 @@ export class MicrosoftDefenderCLI implements IMicrosoftDefenderCLI { successfulExitCodes: number[], additionalArgs: string[] ): Promise { + await setupEnvironment(); + const cliFilePath = process.env['DEFENDER_FILEPATH']; if (!cliFilePath) { diff --git a/src/v2/defender-client.ts b/src/v2/defender-client.ts index ac7743d4..d7acdf30 100644 --- a/src/v2/defender-client.ts +++ b/src/v2/defender-client.ts @@ -103,7 +103,7 @@ async function runDefenderCli( /** * Sets up the environment for the Defender CLI. */ -async function setupEnvironment(): Promise { +export async function setupEnvironment(): Promise { const toolCacheDir = process.env['RUNNER_TOOL_CACHE'] || path.join(os.homedir(), '.defender'); const defenderDir = path.join(toolCacheDir, '_defender'); From 148f542ff38f84063932cca0aee8611735acd161 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 15:10:58 +0200 Subject: [PATCH 09/17] chore: add vulnerable model scan job for bert-tiny-torch-vuln Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index f0c3d0fb..35112802 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -61,3 +61,31 @@ jobs: if: always() with: sarif_file: ${{ steps.defender.outputs.sarifFile }} + + defender-model-scan-vuln: + name: Defender CLI v2 - Model Scan (Vulnerable) + + runs-on: self-hosted + + steps: + + # Checkout your code repository to scan + - uses: actions/checkout@v6 + + # Run Defender CLI v2 model scan on vulnerable model + - name: Run Defender CLI - Model Scan (bert-tiny-torch-vuln) + uses: ./ + id: defender + with: + command: 'model' + modelPath: 'https://huggingface.co/drhyrum/bert-tiny-torch-vuln' + policy: 'mdc' + break: 'false' + pr-summary: 'true' + + # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} From b57d3adc309c96234b28b6e8c4e3979feef98d0b Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 15:30:36 +0200 Subject: [PATCH 10/17] chore: remove upload-sarif from model scan jobs (incompatible URI scheme) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 35112802..919f0409 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -55,13 +55,6 @@ jobs: break: 'false' pr-summary: 'true' - # Upload results to the Security tab - - name: Upload results to Security tab - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: ${{ steps.defender.outputs.sarifFile }} - defender-model-scan-vuln: name: Defender CLI v2 - Model Scan (Vulnerable) @@ -82,10 +75,3 @@ jobs: policy: 'mdc' break: 'false' pr-summary: 'true' - - # Upload results to the Security tab - - name: Upload results to Security tab - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: ${{ steps.defender.outputs.sarifFile }} From 38abab4fa3cc617e1e41e96ec2ba42732697a18e Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Mon, 16 Mar 2026 17:48:26 +0200 Subject: [PATCH 11/17] chore: add filesystem scan job with azuredevops policy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/self-hosted-validation.yml | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation.yml index 919f0409..2ce04ffe 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation.yml @@ -75,3 +75,30 @@ jobs: policy: 'mdc' break: 'false' pr-summary: 'true' + + defender-fs-scan: + name: Defender CLI v2 - Filesystem Scan + + runs-on: self-hosted + + steps: + + # Checkout your code repository to scan + - uses: actions/checkout@v6 + + # Run Defender CLI v2 filesystem scan + - name: Run Defender CLI - Filesystem Scan + uses: ./ + id: defender + with: + command: 'fs' + policy: 'azuredevops' + break: 'false' + pr-summary: 'true' + + # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} From cfe50ee21858299c6a46449ece6553aee6a532bf Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Tue, 17 Mar 2026 15:15:22 +0200 Subject: [PATCH 12/17] refactor: split validation into v1/v2 workflows, restore v1 action.yml - Revert action.yml to v1 MSDO inputs (paths updated to lib/v1/) - Create v2/action.yml for Defender CLI v2 - Split self-hosted-validation into v1 and v2 workflows - v1 workflow uses ./ (root action.yml) - v2 workflow uses ./v2/ (v2 action.yml) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/self-hosted-validation-v1.yml | 29 +++++++++++ ...tion.yml => self-hosted-validation-v2.yml} | 10 ++-- action.yml | 48 ++++++++----------- v2/action.yml | 41 ++++++++++++++++ 4 files changed, 96 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/self-hosted-validation-v1.yml rename .github/workflows/{self-hosted-validation.yml => self-hosted-validation-v2.yml} (95%) create mode 100644 v2/action.yml diff --git a/.github/workflows/self-hosted-validation-v1.yml b/.github/workflows/self-hosted-validation-v1.yml new file mode 100644 index 00000000..67d1254c --- /dev/null +++ b/.github/workflows/self-hosted-validation-v1.yml @@ -0,0 +1,29 @@ +name: MSDO v1 self-hosted validation +on: push + +permissions: + id-token: write + security-events: write + +jobs: + msdo-scan: + name: MSDO v1 - Security Scan + + runs-on: self-hosted + + steps: + + # Checkout your code repository to scan + - uses: actions/checkout@v6 + + # Run MSDO v1 + - name: Run MSDO + uses: ./ + id: msdo + + # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} diff --git a/.github/workflows/self-hosted-validation.yml b/.github/workflows/self-hosted-validation-v2.yml similarity index 95% rename from .github/workflows/self-hosted-validation.yml rename to .github/workflows/self-hosted-validation-v2.yml index 2ce04ffe..1e6ebac6 100644 --- a/.github/workflows/self-hosted-validation.yml +++ b/.github/workflows/self-hosted-validation-v2.yml @@ -1,4 +1,4 @@ -name: Microsoft Defender CLI v2 self-hosted validation +name: Defender CLI v2 self-hosted validation on: push permissions: @@ -18,7 +18,7 @@ jobs: # Run Defender CLI v2 image scan - name: Run Defender CLI - Image Scan - uses: ./ + uses: ./v2/ id: defender with: command: 'image' @@ -46,7 +46,7 @@ jobs: # Run Defender CLI v2 model scan - name: Run Defender CLI - Model Scan - uses: ./ + uses: ./v2/ id: defender with: command: 'model' @@ -67,7 +67,7 @@ jobs: # Run Defender CLI v2 model scan on vulnerable model - name: Run Defender CLI - Model Scan (bert-tiny-torch-vuln) - uses: ./ + uses: ./v2/ id: defender with: command: 'model' @@ -88,7 +88,7 @@ jobs: # Run Defender CLI v2 filesystem scan - name: Run Defender CLI - Filesystem Scan - uses: ./ + uses: ./v2/ id: defender with: command: 'fs' diff --git a/action.yml b/action.yml index 0771bcd2..d372b4a6 100644 --- a/action.yml +++ b/action.yml @@ -1,41 +1,35 @@ name: 'security-devops-action' -description: 'Run Microsoft Defender for DevOps security scans.' +description: 'Run security analyzers.' author: 'Microsoft' branding: icon: 'shield' color: 'black' inputs: command: - description: 'The scan type to perform. Options: fs (filesystem), image (container image), model (AI model).' - default: 'fs' - fileSystemPath: - description: 'The filesystem path to scan. Used when command is fs.' - default: ${{ github.workspace }} - imageName: - description: 'The container image name to scan. Used when command is image. Example: nginx:latest' - modelPath: - description: 'The AI model path or URL to scan. Used when command is model. Supports local paths and http:// or https:// URLs.' + description: Deprecated, do not use. + config: + description: A file path to a .gdnconfig file. policy: - description: 'The name of the well known policy to use. Options: github, microsoft, none.' - default: 'github' - break: - description: 'If true, the action will fail the build when critical vulnerabilities are detected.' - default: 'false' - debug: - description: 'Enable debug logging for verbose output.' - default: 'false' - pr-summary: - description: 'Post a vulnerability summary to the GitHub Job Summary.' - default: 'true' - args: - description: 'Additional arguments to pass to the Defender CLI.' + description: The name of the well known policy to use. Defaults to GitHub. + default: GitHub + categories: + description: A comma separated list of analyzer categories to run. Values secrets, code, artifacts, IaC, containers. Example IaC,secrets. Defaults to all. + languages: + description: A comma separated list of languages to analyze. Example javascript, typescript. Defaults to all. tools: - description: 'A comma separated list of tools. Used for container-mapping backward compatibility.' + description: A comma separated list of analyzer to run. Example bandit, binskim, container-mapping, eslint, templateanalyzer, terrascan, trivy. + includeTools: + description: Deprecated + break-on-detections: + description: If true, the action will fail the build when vulnerabilities are detected at or above the configured severity. Requires toolkit support for MSDO_BREAK. + default: 'false' + existingFilename: + description: A SARIF filename that already exists. If it does, then the normal run will not take place and the file will instead be uploaded to MSDO backend. outputs: sarifFile: description: A file path to a SARIF results file. runs: using: 'node20' - main: 'lib/v2/defender-main.js' - pre: 'lib/v2/pre.js' - post: 'lib/v2/post.js' + main: 'lib/v1/main.js' + pre: 'lib/v1/pre.js' + post: 'lib/v1/post.js' diff --git a/v2/action.yml b/v2/action.yml new file mode 100644 index 00000000..622d36f1 --- /dev/null +++ b/v2/action.yml @@ -0,0 +1,41 @@ +name: 'security-devops-action-v2' +description: 'Run Microsoft Defender for DevOps security scans.' +author: 'Microsoft' +branding: + icon: 'shield' + color: 'black' +inputs: + command: + description: 'The scan type to perform. Options: fs (filesystem), image (container image), model (AI model).' + default: 'fs' + fileSystemPath: + description: 'The filesystem path to scan. Used when command is fs.' + default: ${{ github.workspace }} + imageName: + description: 'The container image name to scan. Used when command is image. Example: nginx:latest' + modelPath: + description: 'The AI model path or URL to scan. Used when command is model. Supports local paths and http:// or https:// URLs.' + policy: + description: 'The name of the well known policy to use. Options: github, microsoft, none.' + default: 'github' + break: + description: 'If true, the action will fail the build when critical vulnerabilities are detected.' + default: 'false' + debug: + description: 'Enable debug logging for verbose output.' + default: 'false' + pr-summary: + description: 'Post a vulnerability summary to the GitHub Job Summary.' + default: 'true' + args: + description: 'Additional arguments to pass to the Defender CLI.' + tools: + description: 'A comma separated list of tools. Used for container-mapping backward compatibility.' +outputs: + sarifFile: + description: A file path to a SARIF results file. +runs: + using: 'node20' + main: '../lib/v2/defender-main.js' + pre: '../lib/v2/pre.js' + post: '../lib/v2/post.js' From 0d4071e7e0000593a1393cd98de30d7516d4e104 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Tue, 17 Mar 2026 17:33:28 +0200 Subject: [PATCH 13/17] chore: gitignore copilot-instructions.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 32 -------------------------------- .gitignore | 3 +++ 2 files changed, 3 insertions(+), 32 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index ac64f605..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,32 +0,0 @@ -# Copilot Instructions - -## Build & Test - -```bash -npm run build # Gulp: clean → sideload → compile (src/ → lib/) -npm run buildAndTest # Build + run tests -npm run buildTests # Build including test compilation -npm test # Run tests only (mocha **/*.tests.js) -npx mocha test/pre.tests.js # Run a single test file -``` - -The `@microsoft/security-devops-actions-toolkit` package comes from GitHub Packages (configured in `.npmrc`). You need a GitHub token with `read:packages` scope to `npm install`. - -## Architecture - -This is a **GitHub Action** (node20) with a three-phase lifecycle defined in `action.yml`: - -- **pre** (`pre.ts`) → runs `ContainerMapping.runPreJob()` — saves job start timestamp -- **main** (`main.ts`) → runs `MicrosoftSecurityDevOps.runMain()` — invokes the MSDO CLI with user-configured tools/categories/languages -- **post** (`post.ts`) → runs `ContainerMapping.runPostJob()` — collects Docker events/images since pre-job and reports to Defender for DevOps - -Both `MicrosoftSecurityDevOps` and `ContainerMapping` implement the `IMicrosoftSecurityDevOps` interface. The factory function `getExecutor()` in `msdo-interface.ts` instantiates them. The `container-mapping` tool is special: it runs only in pre/post phases, not through the MSDO CLI. When it's the only tool specified, `main.ts` skips execution entirely. - -The heavy lifting (CLI installation, execution, SARIF processing) lives in the `@microsoft/security-devops-actions-toolkit` package — this repo is the GitHub Action wrapper. - -## Conventions - -- **`lib/` is committed** — the official build workflow compiles TypeScript and commits the JS output to the branch. Don't add `lib/` to `.gitignore`. -- **Test files use `.tests.ts`** suffix (not `.test.ts`). Tests live in `test/` with a separate `tsconfig.json`. Compiled test JS is gitignored. -- **Testing stack**: Mocha + Sinon. Tests stub `@actions/core`, `@actions/exec`, and `https` to avoid real GitHub Action or network calls. -- **Sideloading**: Set `SECURITY_DEVOPS_ACTION_BUILD_SIDELOAD=true` to build and link a local clone of `security-devops-actions-toolkit` (expected as a sibling directory). This is handled in `gulpfile.js`. diff --git a/.gitignore b/.gitignore index de81b306..66f5e3c1 100644 --- a/.gitignore +++ b/.gitignore @@ -332,3 +332,6 @@ ASALocalRun/ # GitHub Actions Runner actions-runner/ + +# Copilot instructions +.github/copilot-instructions.md From 57c1be2394f73c2ba747398e7c5cb4476204fb68 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Wed, 18 Mar 2026 10:20:42 +0200 Subject: [PATCH 14/17] chore: add comprehensive v2 test variations - Policy variations: github, microsoft, none, azuredevops, mdc - Break on critical: image (vuln), model (vuln), fs - Debug logging: image with debug=true - PR summary toggle: image with pr-summary=false - Custom args: image with --defender-list-findings - Different images: nginx, pycontribs/ubuntu (vulnerable) - Defaults only: no inputs (verify all defaults) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/self-hosted-validation-v2.yml | 320 +++++++++++++++++- 1 file changed, 306 insertions(+), 14 deletions(-) diff --git a/.github/workflows/self-hosted-validation-v2.yml b/.github/workflows/self-hosted-validation-v2.yml index 1e6ebac6..b5e3d8d4 100644 --- a/.github/workflows/self-hosted-validation-v2.yml +++ b/.github/workflows/self-hosted-validation-v2.yml @@ -6,17 +6,17 @@ permissions: security-events: write jobs: + # === Existing scan jobs === + defender-image-scan: - name: Defender CLI v2 - Image Scan + name: Image Scan (mdc policy) runs-on: self-hosted steps: - # Checkout your code repository to scan - uses: actions/checkout@v6 - # Run Defender CLI v2 image scan - name: Run Defender CLI - Image Scan uses: ./v2/ id: defender @@ -27,7 +27,6 @@ jobs: break: 'false' pr-summary: 'true' - # Upload results to the Security tab - name: Upload results to Security tab uses: github/codeql-action/upload-sarif@v3 if: always() @@ -35,16 +34,14 @@ jobs: sarif_file: ${{ steps.defender.outputs.sarifFile }} defender-model-scan: - name: Defender CLI v2 - Model Scan + name: Model Scan (clean - Qwen) runs-on: self-hosted steps: - # Checkout your code repository to scan - uses: actions/checkout@v6 - # Run Defender CLI v2 model scan - name: Run Defender CLI - Model Scan uses: ./v2/ id: defender @@ -56,16 +53,14 @@ jobs: pr-summary: 'true' defender-model-scan-vuln: - name: Defender CLI v2 - Model Scan (Vulnerable) + name: Model Scan (vulnerable - bert-tiny-torch-vuln) runs-on: self-hosted steps: - # Checkout your code repository to scan - uses: actions/checkout@v6 - # Run Defender CLI v2 model scan on vulnerable model - name: Run Defender CLI - Model Scan (bert-tiny-torch-vuln) uses: ./v2/ id: defender @@ -77,16 +72,14 @@ jobs: pr-summary: 'true' defender-fs-scan: - name: Defender CLI v2 - Filesystem Scan + name: FS Scan (azuredevops policy) runs-on: self-hosted steps: - # Checkout your code repository to scan - uses: actions/checkout@v6 - # Run Defender CLI v2 filesystem scan - name: Run Defender CLI - Filesystem Scan uses: ./v2/ id: defender @@ -96,7 +89,306 @@ jobs: break: 'false' pr-summary: 'true' - # Upload results to the Security tab + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Policy variations === + + fs-policy-github: + name: FS Scan (github policy - default) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - FS with github policy + uses: ./v2/ + id: defender + with: + command: 'fs' + policy: 'github' + break: 'false' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + fs-policy-microsoft: + name: FS Scan (microsoft policy) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - FS with microsoft policy + uses: ./v2/ + id: defender + with: + command: 'fs' + policy: 'microsoft' + break: 'false' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + fs-policy-none: + name: FS Scan (no policy) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - FS with no policy + uses: ./v2/ + id: defender + with: + command: 'fs' + policy: 'none' + break: 'false' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Break on critical === + + image-break-vuln: + name: Image Scan (break=true, vulnerable image) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - Image with break (should fail) + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'pycontribs/ubuntu:latest' + policy: 'mdc' + break: 'true' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + model-break-vuln: + name: Model Scan (break=true, vulnerable model) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - Model with break (should fail) + uses: ./v2/ + id: defender + with: + command: 'model' + modelPath: 'https://huggingface.co/drhyrum/bert-tiny-torch-vuln' + policy: 'mdc' + break: 'true' + pr-summary: 'true' + + fs-break: + name: FS Scan (break=true) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - FS with break + uses: ./v2/ + id: defender + with: + command: 'fs' + policy: 'github' + break: 'true' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Debug logging === + + image-debug: + name: Image Scan (debug=true) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - Image with debug + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'ubuntu:latest' + policy: 'mdc' + break: 'false' + debug: 'true' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === PR summary toggle === + + image-no-summary: + name: Image Scan (pr-summary=false) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - Image without summary + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'ubuntu:latest' + policy: 'mdc' + break: 'false' + pr-summary: 'false' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Custom args === + + image-custom-args: + name: Image Scan (custom args --defender-list-findings) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - Image with custom args + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'ubuntu:latest' + policy: 'mdc' + break: 'false' + pr-summary: 'true' + args: '--defender-list-findings' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Different images === + + image-nginx: + name: Image Scan (nginx) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - nginx image + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'nginx:latest' + policy: 'mdc' + break: 'false' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + image-vuln: + name: Image Scan (vulnerable - pycontribs/ubuntu) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - vulnerable image + uses: ./v2/ + id: defender + with: + command: 'image' + imageName: 'pycontribs/ubuntu:latest' + policy: 'mdc' + break: 'false' + pr-summary: 'true' + + - name: Upload results to Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.defender.outputs.sarifFile }} + + # === Defaults only === + + defaults-only: + name: Defaults Only (no inputs) + + runs-on: self-hosted + + steps: + + - uses: actions/checkout@v6 + + - name: Run Defender CLI - defaults + uses: ./v2/ + id: defender + - name: Upload results to Security tab uses: github/codeql-action/upload-sarif@v3 if: always() From 3dfd65b48f1454a74ec7d13a4a63ef50f7dab4eb Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Wed, 18 Mar 2026 11:35:03 +0200 Subject: [PATCH 15/17] fix: change default policy from github to mdc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/v2/defender-cli.js | 2 +- src/v2/defender-cli.ts | 4 ++-- v2/action.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/v2/defender-cli.js b/lib/v2/defender-cli.js index 6ffe6ad6..beb9ccfd 100644 --- a/lib/v2/defender-cli.js +++ b/lib/v2/defender-cli.js @@ -112,7 +112,7 @@ class MicrosoftDefenderCLI { } let successfulExitCodes = [0]; const outputPath = path.join(process.env['RUNNER_TEMP'] || process.cwd(), 'defender.sarif'); - const policyInput = core.getInput(defender_helpers_1.Inputs.Policy) || 'github'; + const policyInput = core.getInput(defender_helpers_1.Inputs.Policy) || 'mdc'; let policy; if (policyInput === 'none') { policy = ''; diff --git a/src/v2/defender-cli.ts b/src/v2/defender-cli.ts index e6e4c0e2..abb09384 100644 --- a/src/v2/defender-cli.ts +++ b/src/v2/defender-cli.ts @@ -113,8 +113,8 @@ export class MicrosoftDefenderCLI implements IMicrosoftDefenderCLI { 'defender.sarif' ); - // Get policy from input, default to 'github' - const policyInput: string = core.getInput(Inputs.Policy) || 'github'; + // Get policy from input, default to 'mdc' + const policyInput: string = core.getInput(Inputs.Policy) || 'mdc'; let policy: string; if (policyInput === 'none') { policy = ''; diff --git a/v2/action.yml b/v2/action.yml index 622d36f1..c3abadd5 100644 --- a/v2/action.yml +++ b/v2/action.yml @@ -17,7 +17,7 @@ inputs: description: 'The AI model path or URL to scan. Used when command is model. Supports local paths and http:// or https:// URLs.' policy: description: 'The name of the well known policy to use. Options: github, microsoft, none.' - default: 'github' + default: 'mdc' break: description: 'If true, the action will fail the build when critical vulnerabilities are detected.' default: 'false' From 72731515af708882266d50c0bb2e9c0873b2b51e Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Thu, 19 Mar 2026 10:17:48 +0200 Subject: [PATCH 16/17] merge resolution --- action.yml | 2 +- test/post.tests.ts | 1 + test/pre.tests.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index d372b4a6..fd52b505 100644 --- a/action.yml +++ b/action.yml @@ -29,7 +29,7 @@ outputs: sarifFile: description: A file path to a SARIF results file. runs: - using: 'node20' + using: 'node24' main: 'lib/v1/main.js' pre: 'lib/v1/pre.js' post: 'lib/v1/post.js' diff --git a/test/post.tests.ts b/test/post.tests.ts index deb98821..8ec52757 100644 --- a/test/post.tests.ts +++ b/test/post.tests.ts @@ -4,6 +4,7 @@ import sinon from 'sinon'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; import { run, sendReport, _sendReport } from '../lib/v1/post'; +import { ContainerMapping } from '../lib/v1/container-mapping'; describe('postjob run', function() { let execStub: sinon.SinonStub; diff --git a/test/pre.tests.ts b/test/pre.tests.ts index 8a8f00ae..46b9aac8 100644 --- a/test/pre.tests.ts +++ b/test/pre.tests.ts @@ -1,6 +1,7 @@ import sinon from 'sinon'; import * as core from '@actions/core'; import { run } from '../lib/v1/pre'; +import { ContainerMapping } from '../lib/v1/container-mapping'; describe('prejob run', () => { let saveStateStub: sinon.SinonStub; From 3a425438f5d910f79d1a7a8446b32086eb375420 Mon Sep 17 00:00:00 2001 From: Omer Bareket Date: Thu, 19 Mar 2026 10:45:32 +0200 Subject: [PATCH 17/17] remove obselete arch --- lib/v2/defender-installer.js | 2 +- src/v2/defender-installer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/v2/defender-installer.js b/lib/v2/defender-installer.js index 966efe6e..af98f45a 100644 --- a/lib/v2/defender-installer.js +++ b/lib/v2/defender-installer.js @@ -151,7 +151,7 @@ function resolveFileName() { case 'win32': if (arch === 'arm64') return 'Defender_win-arm64.exe'; - if (arch === 'ia32' || arch === 'x32') + if (arch === 'ia32') return 'Defender_win-x86.exe'; return 'Defender_win-x64.exe'; case 'linux': diff --git a/src/v2/defender-installer.ts b/src/v2/defender-installer.ts index ae6c3321..29c96cab 100644 --- a/src/v2/defender-installer.ts +++ b/src/v2/defender-installer.ts @@ -153,7 +153,7 @@ export function resolveFileName(): string { switch (platform) { case 'win32': if (arch === 'arm64') return 'Defender_win-arm64.exe'; - if (arch === 'ia32' || arch === 'x32') return 'Defender_win-x86.exe'; + if (arch === 'ia32') return 'Defender_win-x86.exe'; return 'Defender_win-x64.exe'; case 'linux': if (arch === 'arm64') return 'Defender_linux-arm64';