1+ import * as path from 'path' ;
2+ import * as process from 'process' ;
3+ import * as fs from 'fs' ;
4+ import * as tl from 'azure-pipelines-task-lib/task' ;
5+ import { IExecOptions } from "azure-pipelines-task-lib/toolrunner" ;
6+ import * as common from './msdo-common' ;
7+ import * as installer from './defender-installer' ;
8+
9+ /**
10+ * The default version of Defender CLI to install if no version is specified.
11+ */
12+ const cliVersionDefault : string = 'latest' ;
13+
14+ /**
15+ * Sets up the environment for the Defender CLI run.
16+ * Sets pipeline variables.
17+ * Resolves the version of Defender CLI to install.
18+ * Installs Defender CLI
19+ *
20+ * @param taskFolder The folder of the task that is using the Defender CLI
21+ */
22+ async function setupEnvironment ( ) : Promise < void > {
23+
24+ console . log ( '------------------------------------------------------------------------------' ) ;
25+
26+ // initialize the _defender directory
27+ let agentDirectory = path . join ( process . env . AGENT_ROOTDIRECTORY , '_defender' ) ;
28+ tl . debug ( `agentDirectory = ${ agentDirectory } ` ) ;
29+ common . ensureDirectory ( agentDirectory ) ;
30+
31+ let agentPackagesDirectory = process . env . DEFENDER_PACKAGES_DIRECTORY ;
32+ if ( ! agentPackagesDirectory ) {
33+ agentPackagesDirectory = path . join ( agentDirectory , 'packages' ) ;
34+ tl . debug ( `agentPackagesDirectory = ${ agentPackagesDirectory } ` ) ;
35+ common . ensureDirectory ( agentPackagesDirectory ) ;
36+ process . env . DEFENDER_PACKAGES_DIRECTORY = agentPackagesDirectory ;
37+ }
38+
39+ if ( ! process . env . DEFENDER_FILEPATH ) {
40+ let cliVersion = resolveCliVersion ( ) ;
41+ await installer . install ( cliVersion ) ;
42+ }
43+
44+ console . log ( '------------------------------------------------------------------------------' ) ;
45+ }
46+
47+ /**
48+ * Resolves the version of Defender CLI to install.
49+ *
50+ * @returns The version of Defender CLI to install
51+ */
52+ function resolveCliVersion ( ) : string {
53+ let cliVersion = cliVersionDefault ;
54+
55+ console . log ( `Initial CLI version (default): ${ cliVersion } ` ) ;
56+
57+ if ( process . env . DEFENDER_VERSION ) {
58+ cliVersion = process . env . DEFENDER_VERSION ;
59+ console . log ( `Using DEFENDER_VERSION: ${ cliVersion } ` ) ;
60+ }
61+
62+ if ( cliVersion . includes ( '*' ) ) {
63+ console . log ( `Version contains '*', switching to Latest` ) ;
64+ cliVersion = cliVersionDefault ;
65+ }
66+
67+ return cliVersion ;
68+ }
69+
70+ /**
71+ * Gets the path to the Defender CLI
72+ *
73+ * @returns The path to the Defender CLI
74+ */
75+ function getCliFilePath ( ) : string {
76+ let cliFilePath : string = process . env . DEFENDER_FILEPATH ;
77+ tl . debug ( `cliFilePath = ${ cliFilePath } ` ) ;
78+ return cliFilePath ;
79+ }
80+ /**
81+ * Runs a Defender scan with the specified scan type and target
82+ * @param scanType - The type of scan to perform (e.g., "fs", "image")
83+ * @param target - The target to scan (directory path or image name)
84+ * @param policy - The policy to use for scanning (default: "mdc")
85+ * @param outputPath - The output SARIF file path
86+ * @param successfulExitCodes - The exit codes that are considered successful. Defaults to [0]. All others will throw an Error.
87+ * @param additionalArgs - Optional additional CLI arguments to append to the command
88+ */
89+ async function scan (
90+ scanType : string ,
91+ target : string ,
92+ policy : string = 'mdc' ,
93+ outputPath ?: string ,
94+ successfulExitCodes : number [ ] = null ,
95+ additionalArgs : string [ ] = [ ]
96+ ) : Promise < void > {
97+
98+ if ( ! outputPath ) {
99+ outputPath = path . join ( process . env . BUILD_STAGINGDIRECTORY || process . cwd ( ) , 'defender.sarif' ) ;
100+ }
101+
102+ let args = [
103+ 'scan' ,
104+ scanType ,
105+ target ,
106+ '--defender-policy' , policy ,
107+ '--defender-output' , outputPath
108+ ] ;
109+
110+ // Append additional arguments if provided
111+ if ( additionalArgs && additionalArgs . length > 0 ) {
112+ args = args . concat ( additionalArgs ) ;
113+ tl . debug ( `Appending additional arguments: ${ additionalArgs . join ( ' ' ) } ` ) ;
114+ }
115+
116+ await runDefenderCli ( args , successfulExitCodes ) ;
117+ }
118+
119+ /**
120+ * Runs the Defender CLI with the specified arguments for directory scanning
121+ * @param directoryPath - The directory path to scan
122+ * @param policy - The policy to use for scanning (default: "mdc")
123+ * @param outputPath - The output SARIF file path
124+ * @param successfulExitCodes - The exit codes that are considered successful. Defaults to [0]. All others will throw an Error.
125+ * @param additionalArgs - Optional additional CLI arguments to append to the command
126+ */
127+ export async function scanDirectory (
128+ directoryPath : string ,
129+ policy : string = 'mdc' ,
130+ outputPath ?: string ,
131+ successfulExitCodes : number [ ] = null ,
132+ additionalArgs : string [ ] = [ ]
133+ ) : Promise < void > {
134+ await scan ( 'fs' , directoryPath , policy , outputPath , successfulExitCodes , additionalArgs ) ;
135+ }
136+
137+ /**
138+ * Runs the Defender CLI with the specified arguments for container image scanning
139+ * @param imageName - The container image name to scan
140+ * @param policy - The policy to use for scanning (default: "mdc")
141+ * @param outputPath - The output SARIF file path
142+ * @param successfulExitCodes - The exit codes that are considered successful. Defaults to [0]. All others will throw an Error.
143+ * @param additionalArgs - Optional additional CLI arguments to append to the command
144+ */
145+ export async function scanImage (
146+ imageName : string ,
147+ policy : string = 'mdc' ,
148+ outputPath ?: string ,
149+ successfulExitCodes : number [ ] = null ,
150+ additionalArgs : string [ ] = [ ]
151+ ) : Promise < void > {
152+ await scan ( 'image' , imageName , policy , outputPath , successfulExitCodes , additionalArgs ) ;
153+ }
154+
155+ /**
156+ * Runs the Defender CLI with the specified arguments
157+ * @param inputArgs - The CLI arguments to pass to the Defender CLI
158+ * @param successfulExitCodes - The exit codes that are considered successful. Defaults to [0]. All others will throw an Error.
159+ */
160+ async function runDefenderCli ( inputArgs : string [ ] , successfulExitCodes : number [ ] = null ) : Promise < void > {
161+ let tool = null ;
162+
163+ try {
164+
165+ if ( successfulExitCodes == null ) {
166+ successfulExitCodes = [ 0 ] ;
167+ }
168+
169+ await setupEnvironment ( ) ;
170+
171+ let cliFilePath = getCliFilePath ( ) ;
172+
173+ tool = tl . tool ( cliFilePath ) ;
174+
175+ if ( inputArgs != null ) {
176+ for ( let i = 0 ; i < inputArgs . length ; i ++ ) {
177+ tool . arg ( inputArgs [ i ] ) ;
178+ }
179+ }
180+
181+ let systemDebug = tl . getVariable ( "system.debug" ) ;
182+
183+ if ( systemDebug == 'true' ) {
184+ // Add verbose logging if system debug is enabled
185+ tool . arg ( '--defender-debug' ) ;
186+ }
187+
188+ } catch ( error ) {
189+ console . error ( 'Exception occurred while initializing Defender CLI:' ) ;
190+ tl . setResult ( tl . TaskResult . Failed , error ) ;
191+ return ;
192+ }
193+
194+ try {
195+ // let us parse the exit code
196+ let options : IExecOptions = < IExecOptions > {
197+ ignoreReturnCode : true
198+ } ;
199+
200+ tl . debug ( 'Running Microsoft Defender CLI...' ) ;
201+
202+ let exitCode = await tool . exec ( options ) ;
203+
204+ let success = false ;
205+ for ( let i = 0 ; i < successfulExitCodes . length ; i ++ ) {
206+ if ( exitCode == successfulExitCodes [ i ] ) {
207+ success = true ;
208+ break ;
209+ }
210+ }
211+
212+ if ( ! success ) {
213+ throw new Error ( `Defender CLI exited with an error exit code: ${ exitCode } ` ) ;
214+ }
215+ } catch ( error ) {
216+ tl . setResult ( tl . TaskResult . Failed , error ) ;
217+ }
218+ }
0 commit comments