diff --git a/.github/workflows/self-hosted-validation-v2.yml b/.github/workflows/self-hosted-validation-v2.yml deleted file mode 100644 index 12476bd2..00000000 --- a/.github/workflows/self-hosted-validation-v2.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Defender CLI v2 self-hosted validation -on: - push: - branches: [main, 'release/**'] - workflow_dispatch: - -permissions: - id-token: write - security-events: write - -jobs: - defender-image-scan: - name: Image Scan (mdc policy) - - runs-on: self-hosted - - steps: - - - uses: actions/checkout@v6.0.2 - - - name: Run Defender CLI - Image Scan - uses: ./v2/ - id: defender - with: - command: 'image' - imageName: '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 }} diff --git a/lib/v2/container-mapping.js b/lib/v2/container-mapping.js deleted file mode 100644 index 14a8c2a5..00000000 --- a/lib/v2/container-mapping.js +++ /dev/null @@ -1,268 +0,0 @@ -"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) => { - let apiTime = Date.now(); - let options = { - method: 'POST', - timeout: 2500, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + bearerToken, - 'Content-Length': Buffer.byteLength(data, 'utf8') - } - }; - 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: ' + (Date.now() - 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) => { - 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 deleted file mode 100644 index 54a5b0c5..00000000 --- a/lib/v2/defender-cli.js +++ /dev/null @@ -1,166 +0,0 @@ -"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 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) || 'mdc'; - 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'); - 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: - 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 (0, defender_client_1.scanModel)(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; - } - }); - } -} -exports.MicrosoftDefenderCLI = MicrosoftDefenderCLI; diff --git a/lib/v2/defender-client.js b/lib/v2/defender-client.js deleted file mode 100644 index bfaaf8ad..00000000 --- a/lib/v2/defender-client.js +++ /dev/null @@ -1,128 +0,0 @@ -"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.setupEnvironment = exports.scanModel = 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 scanModel(modelPath, policy, outputPath, successfulExitCodes, additionalArgs) { - return __awaiter(this, void 0, void 0, function* () { - yield scan('model', modelPath, policy, outputPath, successfulExitCodes, additionalArgs); - }); -} -exports.scanModel = scanModel; -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); - } - }); -} -exports.setupEnvironment = setupEnvironment; -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 deleted file mode 100644 index f736eaf8..00000000 --- a/lib/v2/defender-helpers.js +++ /dev/null @@ -1,174 +0,0 @@ -"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 deleted file mode 100644 index f519de87..00000000 --- a/lib/v2/defender-installer.js +++ /dev/null @@ -1,239 +0,0 @@ -"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 crypto = __importStar(require("crypto")); -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); - yield verifyIntegrity(filePath, downloadUrl); - 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')); - } - const allowedHost = new URL(downloadBaseUrl).hostname; - const redirectHost = new URL(redirectUrl).hostname; - if (redirectHost !== allowedHost) { - return reject(new Error(`Redirect to untrusted host: ${redirectHost}. Expected: ${allowedHost}`)); - } - 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 verifyIntegrity(filePath, downloadUrl) { - return __awaiter(this, void 0, void 0, function* () { - const checksumUrl = `${downloadUrl}.sha256`; - const expectedHash = yield downloadString(checksumUrl); - const expected = expectedHash.trim().split(/\s+/)[0].toLowerCase(); - const fileBuffer = fs.readFileSync(filePath); - const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); - if (actualHash !== expected) { - fs.unlinkSync(filePath); - throw new Error(`Integrity check failed for ${path.basename(filePath)}: expected ${expected}, got ${actualHash}`); - } - core.debug(`Integrity verified: ${actualHash}`); - }); -} -function downloadString(url) { - return new Promise((resolve, reject) => { - const request = https.get(url, { timeout: downloadTimeoutMs }, (response) => { - if (response.statusCode === 301 || response.statusCode === 302) { - const redirectUrl = response.headers.location; - if (!redirectUrl) { - return reject(new Error('Redirect without location header')); - } - const allowedHost = new URL(downloadBaseUrl).hostname; - const redirectHost = new URL(redirectUrl).hostname; - if (redirectHost !== allowedHost) { - return reject(new Error(`Redirect to untrusted host: ${redirectHost}. Expected: ${allowedHost}`)); - } - core.debug(`Following redirect to: ${redirectUrl}`); - downloadString(redirectUrl).then(resolve).catch(reject); - return; - } - if (response.statusCode !== 200) { - return reject(new Error(`Download failed with status code: ${response.statusCode}`)); - } - const chunks = []; - response.on('data', (chunk) => chunks.push(chunk)); - response.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); - response.on('error', (error) => reject(new Error(`Download error: ${error.message}`))); - }); - request.on('error', (error) => { - reject(new Error(`Download error: ${error.message}`)); - }); - request.on('timeout', () => { - request.destroy(); - 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') - 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 deleted file mode 100644 index 6b0ba53d..00000000 --- a/lib/v2/defender-interface.js +++ /dev/null @@ -1,7 +0,0 @@ -"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 deleted file mode 100644 index 4d03025f..00000000 --- a/lib/v2/defender-main.js +++ /dev/null @@ -1,59 +0,0 @@ -"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 deleted file mode 100644 index 63d7f32b..00000000 --- a/lib/v2/job-summary.js +++ /dev/null @@ -1,277 +0,0 @@ -"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 deleted file mode 100644 index 114788ab..00000000 --- a/lib/v2/post.js +++ /dev/null @@ -1,45 +0,0 @@ -"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 deleted file mode 100644 index 9160a24c..00000000 --- a/lib/v2/pre.js +++ /dev/null @@ -1,45 +0,0 @@ -"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/v2/container-mapping.ts b/src/v2/container-mapping.ts deleted file mode 100644 index 11510cd9..00000000 --- a/src/v2/container-mapping.ts +++ /dev/null @@ -1,292 +0,0 @@ -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((resolve, reject) => { - let apiTime = Date.now(); - let options = { - method: 'POST', - timeout: 2500, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + bearerToken, - 'Content-Length': Buffer.byteLength(data, 'utf8') - } - }; - 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: ' + (Date.now() - 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((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 deleted file mode 100644 index dfd22a8d..00000000 --- a/src/v2/defender-cli.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as core from '@actions/core'; -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, scanModel } 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 'mdc' - const policyInput: string = core.getInput(Inputs.Policy) || 'mdc'; - 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'); - - // 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: - await scanDirectory(target, policy, outputPath, successfulExitCodes, additionalArgs); - break; - - case ScanType.Image: - await scanImage(target, policy, outputPath, successfulExitCodes, additionalArgs); - break; - - case ScanType.Model: - await scanModel(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; - } - } - -} diff --git a/src/v2/defender-client.ts b/src/v2/defender-client.ts deleted file mode 100644 index b5e833c5..00000000 --- a/src/v2/defender-client.ts +++ /dev/null @@ -1,156 +0,0 @@ -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); -} - -/** - * Scans an AI model for security vulnerabilities. - */ -export async function scanModel( - modelPath: string, - policy?: string, - outputPath?: string, - successfulExitCodes?: number[], - additionalArgs?: string[] -): Promise { - await scan('model', modelPath, 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. - */ -export 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 deleted file mode 100644 index d236c972..00000000 --- a/src/v2/defender-helpers.ts +++ /dev/null @@ -1,215 +0,0 @@ -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 deleted file mode 100644 index 613c8a92..00000000 --- a/src/v2/defender-installer.ts +++ /dev/null @@ -1,261 +0,0 @@ -import * as core from '@actions/core'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import * as https from 'https'; -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); - - await verifyIntegrity(filePath, downloadUrl); - - // 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')); - } - // Validate redirect stays on trusted host - const allowedHost = new URL(downloadBaseUrl).hostname; - const redirectHost = new URL(redirectUrl).hostname; - if (redirectHost !== allowedHost) { - return reject(new Error(`Redirect to untrusted host: ${redirectHost}. Expected: ${allowedHost}`)); - } - 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')); - }); - }); -} - -/** - * Verifies the SHA-256 integrity of a downloaded file against a checksum sidecar. - */ -async function verifyIntegrity(filePath: string, downloadUrl: string): Promise { - const checksumUrl = `${downloadUrl}.sha256`; - const expectedHash = await downloadString(checksumUrl); - const expected = expectedHash.trim().split(/\s+/)[0].toLowerCase(); - const fileBuffer = fs.readFileSync(filePath); - const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); - if (actualHash !== expected) { - fs.unlinkSync(filePath); - throw new Error(`Integrity check failed for ${path.basename(filePath)}: expected ${expected}, got ${actualHash}`); - } - core.debug(`Integrity verified: ${actualHash}`); -} - -/** - * Downloads a URL and returns the response body as a string, following redirects with origin pinning. - */ -function downloadString(url: string): Promise { - return new Promise((resolve, reject) => { - const request = https.get(url, { timeout: downloadTimeoutMs }, (response) => { - // Follow redirects (301, 302) - if (response.statusCode === 301 || response.statusCode === 302) { - const redirectUrl = response.headers.location; - if (!redirectUrl) { - return reject(new Error('Redirect without location header')); - } - // Validate redirect stays on trusted host - const allowedHost = new URL(downloadBaseUrl).hostname; - const redirectHost = new URL(redirectUrl).hostname; - if (redirectHost !== allowedHost) { - return reject(new Error(`Redirect to untrusted host: ${redirectHost}. Expected: ${allowedHost}`)); - } - core.debug(`Following redirect to: ${redirectUrl}`); - downloadString(redirectUrl).then(resolve).catch(reject); - return; - } - - if (response.statusCode !== 200) { - return reject(new Error(`Download failed with status code: ${response.statusCode}`)); - } - - const chunks: Buffer[] = []; - response.on('data', (chunk: Buffer) => chunks.push(chunk)); - response.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); - response.on('error', (error) => reject(new Error(`Download error: ${error.message}`))); - }); - - request.on('error', (error) => { - reject(new Error(`Download error: ${error.message}`)); - }); - - request.on('timeout', () => { - request.destroy(); - 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') 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 deleted file mode 100644 index ab9b30f8..00000000 --- a/src/v2/defender-interface.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 deleted file mode 100644 index ffc51e34..00000000 --- a/src/v2/defender-main.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index b29e1988..00000000 --- a/src/v2/job-summary.ts +++ /dev/null @@ -1,392 +0,0 @@ -import * as core from '@actions/core'; -import * as fs from 'fs'; - -/** - * 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 deleted file mode 100644 index f2374316..00000000 --- a/src/v2/post.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index de2eb59b..00000000 --- a/src/v2/pre.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index cdb0ca2d..00000000 --- a/test/defender-client.tests.ts +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index b2639920..00000000 --- a/test/defender-helpers.tests.ts +++ /dev/null @@ -1,180 +0,0 @@ -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 deleted file mode 100644 index c52fb368..00000000 --- a/test/defender-installer.tests.ts +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index d802d39a..00000000 --- a/test/job-summary.tests.ts +++ /dev/null @@ -1,230 +0,0 @@ -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/v2/action.yml b/v2/action.yml deleted file mode 100644 index 1e511a0b..00000000 --- a/v2/action.yml +++ /dev/null @@ -1,41 +0,0 @@ -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: 'Policy to apply. Options: mdc (default), github, microsoft, azuredevops, none.' - default: 'mdc' - 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'