forked from Lerer/veracode-sca
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathsrcclr.ts
More file actions
1055 lines (888 loc) · 46.1 KB
/
srcclr.ts
File metadata and controls
1055 lines (888 loc) · 46.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env node
import { execSync, spawn } from "child_process";
import * as core from '@actions/core'
import { Options } from "./options";
import { SCA_OUTPUT_FILE, run, runText } from "./index";
import * as github from '@actions/github'
import { env } from "process";
import { writeFile } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { writeFileSync } from 'fs';
const runnerOS = process.env.RUNNER_OS;
const cleanCollectors = (inputArr: Array<string>) => {
let allowed: Array<string> = [];
for (var input of inputArr) {
if (input && collectors.indexOf(input.trim().toLowerCase()) > -1) {
allowed.push(input.trim().toLowerCase());
}
}
return allowed;
}
/**
* Extracts the scan URL from the Veracode SCA output
* Looks for a line containing "Full Report Details" followed by a URL
* Also tries to extract from JSON metadata if available
*/
const extractScanUrl = (output: string): string | null => {
core.info('=== Starting URL extraction ===');
if (!output) {
core.info('extractScanUrl: output is empty or null');
return null;
}
core.info(`extractScanUrl: Output length is ${output.length} characters`);
// Pattern to match: "Full Report Details" followed by whitespace and a URL
// More flexible pattern that handles various whitespace amounts
// Matches: "Full Report Details" followed by any whitespace and then a URL starting with http:// or https://
const patterns = [
/Full\s+Report\s+Details\s+(https?:\/\/[^\s\r\n]+)/i, // Explicit URL pattern - most common
/Full\s+Report\s+Details[:\s]+(https?:\/\/[^\s\r\n]+)/i, // With optional colon
/Full\s+Report\s+Details\s+(\S+)/i, // Fallback to any non-whitespace
/Full\s+Report\s+Details[:\s]+(https?:\/\/[^\r\n]+)/i, // Handle newlines
];
// First, check if "Full Report Details" appears in the output at all
const hasFullReport = /Full\s+Report\s+Details/i.test(output);
core.info(`extractScanUrl: "Full Report Details" found in output: ${hasFullReport}`);
if (hasFullReport) {
// Find the line containing "Full Report Details"
const lines = output.split('\n');
const fullReportLine = lines.find(line => /Full\s+Report\s+Details/i.test(line));
if (fullReportLine) {
core.info(`extractScanUrl: Found line: "${fullReportLine.trim()}"`);
}
}
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i];
const match = output.match(pattern);
if (match && match[1]) {
const url = match[1].trim();
// Validate it's a URL
if (url.startsWith('http://') || url.startsWith('https://')) {
core.info(`extractScanUrl: ✓ Found URL using pattern ${i + 1}: ${url}`);
return url;
} else {
core.info(`extractScanUrl: Pattern ${i + 1} matched but result is not a URL: ${url}`);
}
}
}
core.info('extractScanUrl: No URL found in text output, trying JSON fallback');
// Fallback: Try to extract from JSON if available
try {
if (existsSync(SCA_OUTPUT_FILE)) {
core.info(`extractScanUrl: JSON file exists, attempting to read: ${SCA_OUTPUT_FILE}`);
const scaResultsTxt = readFileSync(SCA_OUTPUT_FILE);
const scaResJson = JSON.parse(scaResultsTxt.toString('utf-8'));
if (scaResJson.records && scaResJson.records[0] && scaResJson.records[0].metadata && scaResJson.records[0].metadata.report) {
const url = scaResJson.records[0].metadata.report;
if (url.startsWith('http://') || url.startsWith('https://')) {
core.info(`extractScanUrl: ✓ Found URL in JSON metadata: ${url}`);
return url;
}
} else {
core.info('extractScanUrl: JSON file exists but does not contain report URL in expected structure');
}
} else {
core.info(`extractScanUrl: JSON file does not exist: ${SCA_OUTPUT_FILE}`);
}
} catch (error: any) {
core.info(`extractScanUrl: Error reading JSON fallback: ${error.message || error}`);
}
core.info('extractScanUrl: ✗ No URL found in output or JSON');
core.info('=== URL extraction complete ===');
return null;
}
/**
* TEMPORARY: Sequential dual-scan wrapper for SCA Fix support
* When scaFixEnabled is true, runs txt scan followed by json scan in same action
* TODO: Remove this wrapper when scanner supports native dual output (txt + json simultaneously)
*/
async function runSequentialDualScans(options: Options): Promise<void> {
core.info('=== Starting Sequential Dual-Scan Mode ===');
core.info('Note: Running TXT scan first, then JSON scan sequentially to avoid deadlock');
// Run TXT scan first (skip artifact upload and vuln list generation - will handle both later)
core.info('Step 1: Running TXT scan...');
const txtOptions = { ...options, jsonOutput: false };
await runSingleScan(
txtOptions,
true, // skipArtifactUpload
true // skipVulnListGeneration
);
core.info('✓ TXT scan completed');
// Run JSON scan second
core.info('Step 2: Running JSON scan...');
const jsonOptions = { ...options, jsonOutput: true };
try {
await runSingleScan(
jsonOptions,
true, // skipArtifactUpload
true // skipVulnListGeneration
);
core.info('✓ JSON scan completed');
} catch (jsonError: any) {
core.warning(`JSON scan encountered an issue, but TXT results are available: ${jsonError.message || jsonError}`);
}
// Combine both scan results into single artifact
core.info('Step 3: Uploading combined scan results...');
await combineScanArtifacts();
// Generate vulnerability list after both scans complete
core.info('Step 4: Generating vulnerability list...');
await generateVulnList(options);
}
/**
* Combines both scaResults.txt and scaResults.json into single artifact
* When scanner supports native dual output, this function can be removed
*/
async function combineScanArtifacts(): Promise<void> {
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
const platformType = process.env.PLATFORM_TYPE || 'STANDARD';
if (platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
} else {
artifactClient = new DefaultArtifactClient();
}
const files: string[] = [];
if (existsSync('scaResults.txt')) {
files.push('scaResults.txt');
}
if (existsSync(SCA_OUTPUT_FILE)) {
files.push(SCA_OUTPUT_FILE);
}
if (files.length === 0) {
core.warning('No scan results found to combine');
return;
}
try {
await artifactClient.uploadArtifact(
'Veracode Agent Based SCA Results',
files,
process.cwd(),
{ continueOnError: true }
);
core.info(`✓ Combined artifact uploaded with ${files.length} file(s)`);
} catch (error: any) {
core.warning(`Failed to upload combined artifact: ${error.message || error}`);
}
}
/**
* Helper to upload artifact conditionally
* Skips upload if skipArtifactUpload is true (used in dual-scan mode)
*/
async function uploadArtifactIfNeeded(
artifactClient: any,
artifactName: string,
files: string[],
skipArtifactUpload: boolean,
fileType: 'txt' | 'json'
): Promise<void> {
if (skipArtifactUpload) {
core.info(`Skipping ${fileType.toUpperCase()} artifact upload (will be combined with other scans)`);
return;
}
core.info(`Store ${fileType.toUpperCase()} Results as Artifact`);
try {
await artifactClient.uploadArtifact(artifactName, files, process.cwd(), { continueOnError: true });
} catch (error: any) {
core.warning(`Failed to upload ${fileType} artifact: ${error.message || error}`);
}
}
/**
* Runs a single scan (txt or json based on options.jsonOutput)
* This is the original runAction logic extracted for reuse
* @param options - Scan options
* @param skipArtifactUpload - If true, skip artifact upload (used in dual-scan mode where combineScanArtifacts handles it)
* @param skipVulnListGeneration - If true, skip vulnerability list generation (used in dual-scan mode where it's called after combining)
*/
async function runSingleScan(options: Options, skipArtifactUpload: boolean = false, skipVulnListGeneration: boolean = false): Promise<void> {
try {
core.info('Start command');
let extraCommands: string = '';
if (options.url.length > 0) {
extraCommands = `--url ${options.url} `;
} else {
extraCommands = `${options.path} `;
}
const skip = cleanCollectors(options["skip-collectors"]);
let skipCollectorsAttr = '';
if (skip.length > 0) {
skipCollectorsAttr = `--skip-collectors ${skip.toString()} `;
}
const scan = cleanCollectors(options["scan-collectors"]);
let scanCollectorsAttr = '';
if (scan.length > 0) {
scanCollectorsAttr = `--scan-collectors ${scan.toString()} `;
}
const noGraphs = options["no-graphs"]
const skipVMS = options["skip-vms"]
const shouldGenerateJson = options.createIssues || options.jsonOutput;
const commandOutput = options.createIssues || options.jsonOutput ? `--json=${SCA_OUTPUT_FILE}` : '';
// Artifact name depends on output type: TXT uses standard name, JSON uses sca-fix specific name
const artifactNameBase = options.jsonOutput ? 'Veracode Agent Based SCA Results Json' : 'Veracode Agent Based SCA Results';
extraCommands = `${extraCommands}${options.recursive ? '--recursive ' : ''}${options.quick ? '--quick ' : ''}${options.allowDirty ? '--allow-dirty ' : ''}${options.updateAdvisor ? '--update-advisor ' : ''}${skipVMS ? '--skip-vms ' : ''}${noGraphs ? '--no-graphs ' : ''}${options.debug ? '--debug ' : ''}${skipCollectorsAttr}${scanCollectorsAttr}`;
if (runnerOS == 'Windows') {
const powershellCommand = `powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest https://sca-downloads.veracode.com/ci.ps1 -OutFile $env:TEMP\\ci.ps1; & $env:TEMP\\ci.ps1 -s -- scan ${extraCommands} ${commandOutput}"`
if (shouldGenerateJson) {
core.info('Starting the scan')
let output: string = ''
try {
output = execSync(powershellCommand, { encoding: 'utf-8', maxBuffer: 1024 * 1024 * 10 });//10MB
if (options.createIssues) {
core.info('Create issue "true" - on close')
}
if (core.isDebug()) {
core.info(output);
}
// Extract and set scan URL output
const scanUrl = extractScanUrl(output);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`Scan URL extracted: ${scanUrl}`);
} else {
core.info('Scan URL not found in output');
}
}
catch (error: any) {
if (error.status != null && error.status > 0 && (options.breakBuildOnPolicyFindings == 'true')) {
let summary_info = "Veracode SCA Scan failed with exit code " + error.statuscode + "\n"
core.info(output)
core.setFailed(summary_info)
}
// Try to extract URL even if there was an error
const scanUrl = extractScanUrl(output);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`Scan URL extracted: ${scanUrl}`);
}
}
// PR decoration and issue generation (only if createIssues is enabled)
if (options.createIssues) {
//Pull request decoration
core.info('check if we run on a pull request')
let pullRequest = process.env.GITHUB_REF
let isPR: any = pullRequest?.indexOf("pull")
let summary_message = ""
if (isPR >= 1) {
core.info('We run on a PR, add more messaging')
const context = github.context
const repository: any = process.env.GITHUB_REPOSITORY
const repo = repository.split("/");
const commentID: any = context.payload.pull_request?.number
let pr_header = '<br><br>'
summary_message = `Veracode SCA Scan finished. Please review created and linked issues`
try {
const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
const octokit = github.getOctokit(options.github_token, { baseUrl });
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: commentID,
body: pr_header + summary_message,
});
core.info('Adding scan results message as comment to PR #' + commentID)
} catch (error: any) {
core.info(error);
}
}
else {
summary_message = `Veracode SCA Scan finished. Please review created issues`
}
//Generate issues
run(options, core.info);
core.info(summary_message);
}
// Store output files as artifacts (skip if in dual-scan mode)
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
if (options?.platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
core.info(`Initialized the artifact object using version V1.`);
} else {
artifactClient = new DefaultArtifactClient();
core.info(`Initialized the artifact object using version V2.`);
}
await uploadArtifactIfNeeded(artifactClient, artifactNameBase, ['scaResults.json'], skipArtifactUpload, 'json')
core.info('Finish command');
} else {
core.info('Command to run: ' + powershellCommand)
let output: string = ''
let stderrOutput: string = ''
try {
// execSync captures both stdout and stderr by default, but let's be explicit
output = execSync(powershellCommand, {
encoding: 'utf-8',
maxBuffer: 1024 * 1024 * 10,
stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
});//10MB
core.info(output);
core.info(`Attempting to extract scan URL from output (length: ${output.length} chars)`);
// Extract and set scan URL output
const scanUrl = extractScanUrl(output);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`✓✓✓ SUCCESS: Scan URL extracted and set as output: ${scanUrl}`);
} else {
core.warning('✗✗✗ FAILED: Scan URL not found in output');
// Try to find the line with "Full Report Details" for debugging
const lines = output.split('\n');
const fullReportLine = lines.find(line => line.toLowerCase().includes('full report details'));
if (fullReportLine) {
core.info(`Found "Full Report Details" line: ${fullReportLine}`);
} else {
core.info('"Full Report Details" line not found in output');
}
}
}
catch (error: any) {
// execSync throws on non-zero exit, but output might still be in error.stdout or error.stderr
if (error.stdout) {
output = error.stdout.toString();
}
if (error.stderr) {
stderrOutput = error.stderr.toString();
}
if (error.status != null && error.status > 0 && (options.breakBuildOnPolicyFindings == 'true')) {
let summary_info = "Veracode SCA Scan failed with exit code " + error.statuscode + "\n"
core.setFailed(summary_info)
}
// Try to extract URL from combined output even if there was an error
const combinedOutput = `${output}${stderrOutput}`;
const scanUrl = extractScanUrl(combinedOutput);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`Scan URL extracted from error output: ${scanUrl}`);
} else if (core.isDebug()) {
core.info(`Could not extract URL. Output length: ${output.length}, stderr length: ${stderrOutput.length}`);
}
}
//write output to file
// writeFile('scaResults.txt', output, (err) => {
// if (err) throw err;
// console.log('The file has been saved!');
// });
try {
writeFileSync('scaResults.txt', output);
console.log('The file has been saved!');
} catch (err) {
console.error('Error writing file:', err);
}
// core.info('reading file')
// try {
// const data = readFileSync('scaResults.txt', 'utf8');
// console.log('Full file output: '+data);
// } catch (err) {
// console.error(err);
// }
// Store output files as artifacts (skip if in dual-scan mode)
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
if (options?.platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
core.info(`Initialized the artifact object using version V1.`);
} else {
artifactClient = new DefaultArtifactClient();
core.info(`Initialized the artifact object using version V2.`);
}
await uploadArtifactIfNeeded(artifactClient, artifactNameBase, ['scaResults.txt'], skipArtifactUpload, 'txt')
//Pull request decoration
core.info('check if we run on a pull request')
let pullRequest = process.env.GITHUB_REF
let isPR: any = pullRequest?.indexOf("pull")
if (isPR >= 1) {
core.info("This run is part of a PR, should add some PR comment")
const context = github.context
const repository: any = process.env.GITHUB_REPOSITORY
const repo = repository.split("/");
const commentID: any = context.payload.pull_request?.number
let commentBody = '<br><br>'
commentBody += "<pre>Veracode SCA Scan finished" + "\n"
commentBody += '\n<details><summary>Veracode SCA Scan details</summary><p>\n'
commentBody += output //.replace(/ /g, ' ');
commentBody += '</p></details>\n</pre>'
try {
const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
const octokit = github.getOctokit(options.github_token, { baseUrl });
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: commentID,
body: commentBody,
});
core.info('Adding scan results as comment to PR #' + commentID)
} catch (error: any) {
core.info(error);
}
}
//run(options,core.info);
core.info('Finish command');
}
}
else {
const command = `curl -sSL https://download.sourceclear.com/ci.sh | sh -s -- scan ${extraCommands} ${commandOutput}`;
core.info(command);
if (shouldGenerateJson) {
core.info('Starting the scan')
await new Promise<void>((resolve, reject) => {
const execution = spawn('sh', ['-c', command], {
stdio: "pipe",
shell: false
});
execution.on('error', (data) => {
core.error(data);
reject(data);
})
let output: string = '';
let stderrOutput: string = '';
execution.stdout!.on('data', (data) => {
output = `${output}${data}`;
});
execution.stderr!.on('data', (data) => {
const dataStr = data.toString();
stderrOutput = `${stderrOutput}${dataStr}`;
core.error(`stderr: ${dataStr}`);
});
execution.on('close', async (code) => {
if (options.createIssues) {
core.info('Create issue "true" - on close')
}
if (core.isDebug()) {
core.info(output);
}
// Combine stdout and stderr for URL extraction (URL might be in either)
const combinedOutput = `${output}${stderrOutput}`;
core.info(`Attempting to extract scan URL from combined output (stdout: ${output.length} chars, stderr: ${stderrOutput.length} chars)`);
// Extract and set scan URL output from combined output
const scanUrl = extractScanUrl(combinedOutput);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`✓✓✓ SUCCESS: Scan URL extracted and set as output: ${scanUrl}`);
} else {
core.warning('✗✗✗ FAILED: Scan URL not found in output');
core.info(`Output length: ${output.length}, stderr length: ${stderrOutput.length}, combined: ${combinedOutput.length}`);
// Log a sample of the output to help debug
const fullReportIndex = combinedOutput.indexOf('Full Report');
if (fullReportIndex >= 0) {
const sampleOutput = combinedOutput.substring(Math.max(0, fullReportIndex - 50), Math.min(combinedOutput.length, fullReportIndex + 200));
core.info(`Sample output around "Full Report" (index ${fullReportIndex}): ${sampleOutput}`);
} else {
core.info('"Full Report" text not found in combined output');
}
}
// PR decoration and issue generation (only if createIssues is enabled)
let summary_message = ""
if (options.createIssues) {
//Pull request decoration
core.info('check if we run on a pull request')
let pullRequest = process.env.GITHUB_REF
let isPR: any = pullRequest?.indexOf("pull")
if (isPR >= 1) {
core.info('We run on a PR, add more messaging')
const context = github.context
const repository: any = process.env.GITHUB_REPOSITORY
const repo = repository.split("/");
const commentID: any = context.payload.pull_request?.number
let pr_header = '<br><br>'
summary_message = `Veracode SCA Scan finished with exit code: ${code}. Please review created and linked issues`
try {
const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
const octokit = github.getOctokit(options.github_token, { baseUrl });
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: commentID,
body: pr_header + summary_message,
});
core.info('Adding scan results message as comment to PR #' + commentID)
} catch (error: any) {
core.info(error);
}
}
else {
summary_message = `Veracode SCA Scan finished with exit code: ${code}. Please review created issues`
}
//Generate issues
run(options, core.info);
core.info(summary_message);
}
// if scan was set to fail the pipeline should fail and show a summary of the scan results
if (code != null && code > 0 && (options.breakBuildOnPolicyFindings == 'true')) {
let summary_info = "Veracode SCA Scan failed with exit code " + code + "\n"
core.setFailed(summary_info)
}
// Store output files as artifacts (skip if in dual-scan mode)
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
if (options?.platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
core.info(`Initialized the artifact object using version V1.`);
} else {
artifactClient = new DefaultArtifactClient();
core.info(`Initialized the artifact object using version V2.`);
}
await uploadArtifactIfNeeded(artifactClient, artifactNameBase, ['scaResults.json'], skipArtifactUpload, 'json')
core.info('Finish command');
resolve();
});
});
} else {
core.info('Command to run: ' + command)
await new Promise<void>((resolve, reject) => {
const execution = spawn('sh', ['-c', command], {
stdio: "pipe",
shell: false
});
execution.on('error', (data) => {
core.error(data);
reject(data);
})
let output: string = '';
let stderrOutput: string = '';
execution.stdout!.on('data', (data) => {
const dataStr = data.toString();
output = `${output}${dataStr}`;
// Also log to see output in real-time
core.info(dataStr);
});
execution.stderr!.on('data', (data) => {
const dataStr = data.toString();
stderrOutput = `${stderrOutput}${dataStr}`;
core.error(`stderr: ${dataStr}`);
});
execution.on('close', async (code) => {
//core.info(output);
core.info(`Scan finished with exit code: ${code}`);
// Combine stdout and stderr for URL extraction (URL might be in either)
const combinedOutput = `${output}${stderrOutput}`;
core.info(`Attempting to extract scan URL from combined output (stdout: ${output.length} chars, stderr: ${stderrOutput.length} chars)`);
// Extract and set scan URL output from combined output
const scanUrl = extractScanUrl(combinedOutput);
if (scanUrl) {
core.setOutput('scan-url', scanUrl);
core.info(`✓✓✓ SUCCESS: Scan URL extracted and set as output: ${scanUrl}`);
} else {
core.warning('✗✗✗ FAILED: Scan URL not found in output');
core.info(`Output length: ${output.length}, stderr length: ${stderrOutput.length}, combined: ${combinedOutput.length}`);
// Log a sample of the output to help debug
const fullReportIndex = combinedOutput.indexOf('Full Report');
if (fullReportIndex >= 0) {
const sampleOutput = combinedOutput.substring(Math.max(0, fullReportIndex - 50), Math.min(combinedOutput.length, fullReportIndex + 200));
core.info(`Sample output around "Full Report" (index ${fullReportIndex}): ${sampleOutput}`);
} else {
core.info('"Full Report" text not found in combined output');
}
}
//write output to file
// writeFile('scaResults.txt', output, (err) => {
// if (err) throw err;
// console.log('The file has been saved!');
// });
try {
writeFileSync('scaResults.txt', combinedOutput);
console.log('The file has been saved!');
} catch (err) {
console.error('Error writing file:', err);
}
// Try to extract URL from the file as well (in case output variable missed something)
let fileOutput = combinedOutput;
try {
if (existsSync('scaResults.txt')) {
const fileContent = readFileSync('scaResults.txt', 'utf8');
if (fileContent && fileContent.length > combinedOutput.length) {
fileOutput = fileContent;
if (core.isDebug()) {
core.info('Using file content for URL extraction (file is larger than captured output)');
}
}
}
} catch (err) {
// Ignore file read errors
}
// Re-extract URL from file output if not found in combined output
if (!scanUrl) {
const scanUrlFromFile = extractScanUrl(fileOutput);
if (scanUrlFromFile) {
core.setOutput('scan-url', scanUrlFromFile);
core.info(`Scan URL extracted from file: ${scanUrlFromFile}`);
}
}
// Store output files as artifacts (skip if in dual-scan mode)
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
if (options?.platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
core.info(`Initialized the artifact object using version V1.`);
} else {
artifactClient = new DefaultArtifactClient();
core.info(`Initialized the artifact object using version V2.`);
}
await uploadArtifactIfNeeded(artifactClient, artifactNameBase, ['scaResults.txt'], skipArtifactUpload, 'txt')
//Pull request decoration
core.info('check if we run on a pull request')
let pullRequest = process.env.GITHUB_REF
let isPR: any = pullRequest?.indexOf("pull")
if (isPR >= 1) {
core.info("This run is part of a PR, should add some PR comment")
const context = github.context
const repository: any = process.env.GITHUB_REPOSITORY
const repo = repository.split("/");
const commentID: any = context.payload.pull_request?.number
let commentBody = '<br><br>'
commentBody += "<pre>Veracode SCA Scan finished with exit code " + code + "\n"
commentBody += '\n<details><summary>Veracode SCA Scan details</summary><p>\n'
commentBody += output //.replace(/ /g, ' ');
commentBody += '</p></details>\n</pre>'
try {
const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
const octokit = github.getOctokit(options.github_token, { baseUrl });
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: commentID,
body: commentBody,
});
core.info('Adding scan results as comment to PR #' + commentID)
} catch (error: any) {
core.info(error);
}
}
// if scan was set to fail the pipeline should fail and show a summary of the scan results
if (code != null && code > 0 && (options.breakBuildOnPolicyFindings == 'true')) {
let summary_info = "Veracode SCA Scan failed with exit code " + code + "\n"
core.setFailed(summary_info)
}
//run(options,core.info);
core.info('Finish command');
resolve();
});
});
}
}
// Generate vulnerability list after scan completes (skip in dual-scan mode)
if (!skipVulnListGeneration) {
await generateVulnList(options);
}
} catch (error) {
if (error instanceof Error) {
core.info('Running scan failed.')
//const output = stdout.toString();
core.info(error.message);
//core.setFailed(error.message);
} else {
core.setFailed("unknown error");
console.log(error);
}
}
}
/**
* Main entry point - routes to dual-scan or single-scan based on scaFixEnabled
*/
export async function runAction(options: Options) {
try {
if (options.scaFixEnabled) {
// Temporary dual-scan mode for SCA Fix support
await runSequentialDualScans(options);
} else {
// Standard single scan (backward compatible)
await runSingleScan(options);
}
} catch (error) {
if (error instanceof Error) {
core.setFailed(error.message);
} else {
core.setFailed("Unknown error during scan execution");
}
}
}
/**
* Generates SCA vulnerability list using Veracode CLI
* This function is called at the end of runAction when sca_fix_enabled is true
*/
async function generateVulnList(options: Options): Promise<void> {
try {
core.info('=== Starting SCA Vulnerability List Generation ===');
// Check if sca_fix_enabled is true
if (!options.scaFixEnabled) {
core.info('veracode-sca-fix is NOT enabled, skipping vulnerability list generation');
return;
}
core.info('veracode-sca-fix is enabled, proceeding with vulnerability list generation');
// Check if PR number exists in options
if (!options.prNumber || options.prNumber === 0 || isNaN(options.prNumber)) {
core.info('No PR number found in options, skipping vulnerability list generation');
return;
}
const prNumber = options.prNumber;
core.info(`PR number found: ${prNumber}`);
// Check if scaResults.json exists
if (!existsSync(SCA_OUTPUT_FILE)) {
core.warning(`SCA results file not found: ${SCA_OUTPUT_FILE}. Skipping vulnerability list generation.`);
return;
}
// Check for required environment variables
const veracodeApiKeyId = process.env.VERACODE_API_KEY_ID;
const veracodeApiKeySecret = process.env.VERACODE_API_KEY_SECRET;
if (!veracodeApiKeyId || !veracodeApiKeySecret) {
core.warning('VERACODE_API_KEY_ID or VERACODE_API_KEY_SECRET not set. Skipping vulnerability list generation.');
return;
}
const workingDir = process.cwd();
core.info(`Working directory: ${workingDir}`);
// Check if helper/cli directory exists
const helperCliPath = runnerOS === 'Windows'
? `${workingDir}\\veracode-helper\\helper\\cli`
: `${workingDir}/veracode-helper/helper/cli`;
if (!existsSync(helperCliPath)) {
core.warning(`Helper CLI directory not found at ${helperCliPath}. Skipping vulnerability list generation.`);
return;
}
let cliExecutablePath: string = '';
let veracodeCommand: string;
const vulnListingFile = 'veracode-cli.vuln.listing.json';
if (runnerOS === 'Windows') {
// Windows implementation
// Find the CLI ps1 installer file
const findPs1Command = `powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem -Path '${helperCliPath}' -Filter *.ps1 | Select-Object -First 1 -ExpandProperty FullName"`;
const installerFile = execSync(findPs1Command, { encoding: 'utf-8' }).trim();
if (!installerFile || installerFile === '') {
core.warning(`No CLI ps1 installer file found in ${helperCliPath}. Skipping vulnerability list generation.`);
return;
}
core.info(`Found CLI installer: ${installerFile}`);
// Run the installer to install Veracode CLI
core.info('Running Veracode CLI installer...');
try {
const installCommand = `powershell -NoProfile -ExecutionPolicy Bypass -File "${installerFile}"`;
const installOutput = execSync(installCommand, { encoding: 'utf-8' });
core.info('Veracode CLI installation completed');
if (core.isDebug()) {
core.info(installOutput);
}
} catch (error: any) {
core.warning(`Failed to install Veracode CLI: ${error.message}`);
return;
}
// Check where veracode command is located using Get-Command
core.info('Set veracode.exe command location...');
const appDataPath = process.env.APPDATA || '';
if (!appDataPath) {
core.warning('APPDATA environment variable not found. Skipping vulnerability list generation.');
return;
}
cliExecutablePath = `${appDataPath}\\veracode\\veracode.exe`;
core.info(`Expected Veracode CLI installation path: ${cliExecutablePath}`);
// Verify the CLI was installed
if (!existsSync(cliExecutablePath)) {
core.warning(`Veracode CLI not found at ${cliExecutablePath}. Installation may have failed.`);
return;
}
core.info(`Veracode CLI successfully installed and verified at: ${cliExecutablePath}`);
// Build the veracode fix sca command for Windows using full path
veracodeCommand = `"${cliExecutablePath}" fix sca "${workingDir}" -r "${workingDir}\\${SCA_OUTPUT_FILE}" --list-only --transitive --json "${vulnListingFile}"`;
core.info(`Running command: ${veracodeCommand}`);
} else {
// Linux/Unix implementation
// Find the CLI tar.gz file
const cliFiles = execSync(`ls -1 ${helperCliPath}/*.tar.gz 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
if (!cliFiles) {
core.warning(`No CLI tar.gz file found in ${helperCliPath}. Skipping vulnerability list generation.`);
return;
}
const cliFile = cliFiles.split('\n')[0]; // Get first file
const cliFileName = cliFile.replace('.tar.gz', '').split('/').pop();
core.info(`Found CLI file: ${cliFile}`);
core.info(`Extracting to: ${cliFileName}`);
// Extract the CLI
execSync(`cd ${helperCliPath} && tar -zxf ${cliFile.split('/').pop()}`, { encoding: 'utf-8' });
cliExecutablePath = `${helperCliPath}/${cliFileName}`;
core.info(`CLI executable path: ${cliExecutablePath}`);
// Build the veracode fix sca command
veracodeCommand = `${cliExecutablePath}/veracode fix sca "${workingDir}" -r "${workingDir}/${SCA_OUTPUT_FILE}" --list-only --transitive --json "${vulnListingFile}"`;
core.info(`Running command: ${veracodeCommand}`);
}
// Run the veracode fix sca command
try {
const output = execSync(veracodeCommand, {
encoding: 'utf-8',
env: {
...process.env,
VERACODE_API_KEY_ID: veracodeApiKeyId,
VERACODE_API_KEY_SECRET: veracodeApiKeySecret
}
});
core.info('Veracode CLI execution successful');
if (core.isDebug()) {
core.info(output);
}
// Check if vulnerability listing file was created
if (!existsSync(vulnListingFile)) {
core.warning(`Vulnerability listing file not created: ${vulnListingFile}`);
return;
}
// Upload the vulnerability listing JSON as artifact
core.info('Uploading SCA vulnerability listing JSON as artifact');
const { DefaultArtifactClient } = require('@actions/artifact');
const artifactV1 = require('@actions/artifact-v1');
let artifactClient;
if (options?.platformType === 'ENTERPRISE') {
artifactClient = artifactV1.create();
core.info('Initialized artifact client using version V1');
} else {
artifactClient = new DefaultArtifactClient();
core.info('Initialized artifact client using version V2');
}
const artifactName = 'sca-vuln-listing-json';
const files = [vulnListingFile];
const rootDirectory = workingDir;