Skip to content

Commit 72a46dd

Browse files
authored
Merge pull request #21 from nhinv11/ADO_workitems
Ado workitems
2 parents b220ade + ba5454c commit 72a46dd

File tree

1 file changed

+160
-12
lines changed

1 file changed

+160
-12
lines changed

ado-importer.js

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async function importFlawsToADO(params) {
9191
// Track which work items are still active (not closed)
9292
const activeWorkItems = existingWorkItems.filter(wi => {
9393
const state = wi.fields['System.State'] || 'Unknown';
94-
return state !== 'Closed' && state !== 'Resolved' && state !== 'Removed';
94+
return state !== 'Done' && state !== 'Resolved' && state !== 'Removed';
9595
});
9696
console.log(`Found ${activeWorkItems.length} active work items to check for closure`);
9797

@@ -123,6 +123,7 @@ async function importFlawsToADO(params) {
123123
let createdCount = 0;
124124
let reopenedCount = 0;
125125
let skippedCount = 0;
126+
let closedCount = 0;
126127

127128
if (scanType === 'pipeline') {
128129
const result = await processPipelineFlawsADO(adoPatchClient, adoOrg, adoProject, adoWorkItemType, flawData, {
@@ -140,6 +141,7 @@ async function importFlawsToADO(params) {
140141
createdCount = result.createdCount;
141142
reopenedCount = result.reopenedCount;
142143
skippedCount = result.skippedCount;
144+
closedCount = closePipelineFlaws(adoClient, adoOrg, adoProject, activeWorkItems, result.processedFlawIds, commit_hash, debug)
143145
} else {
144146
const result = await processPolicyFlawsADO(adoPatchClient, adoOrg, adoProject, adoWorkItemType, flawData, {
145147
source_base_path_1,
@@ -359,14 +361,14 @@ function normalizeTitle(title) {
359361
function createVeracodeFlawId(flaw, scanType) {
360362
if (scanType === 'pipeline') {
361363
// For pipeline scans, use CWE:file:line format
362-
const cweId = flaw.cwe_id || 'Unknown';
363-
const fileName = flaw.files?.source_file?.file || 'Unknown';
364-
const lineNumber = flaw.files?.source_file?.line || 'Unknown';
365-
return `[VID:${cweId}:${fileName}:${lineNumber}]`;
364+
const cweName = flaw.issue_type || 'Unknown';
365+
const flawId = flaw.issue_id || 'Unknown';
366+
return `Veracode Flaw (Static): ${cweName}, Flaw ${flawId}`
366367
} else {
367368
// For policy scans, use flaw number format
368-
const flawNumber = flaw.issue_id || 'Unknown';
369-
return `[VID:${flawNumber}]`;
369+
const cweName = flaw.finding_details?.cwe?.name || 'Unknown';
370+
const flawId = flaw.issue_id || 'Unknown';
371+
return `Veracode Flaw (Static): ${cweName}, Flaw ${flawId}`
370372
}
371373
}
372374

@@ -431,6 +433,99 @@ function validateNoDuplicates(existingWorkItems, veracodeFlawId, debug) {
431433
return matches[0] || null;
432434
}
433435

436+
function formatMitigation(annotation){
437+
const mitigationStatus = ['COMMENT', 'FP', 'APPROVED', 'REJECTED']
438+
let mitigation = ''
439+
440+
const created = annotation.created || 'Unknown';
441+
const comment = annotation.comment || 'Unknown';
442+
const action = annotation.action || 'Unknown';
443+
const user_name = annotation.user_name || 'Unknown';
444+
445+
const technique = annotation.technique || 'Unknown';
446+
const specifics = annotation.specifics || 'Unknown';
447+
const remaining_risk = annotation.remaining_risk || 'Unknown';
448+
const verification = annotation.verification || 'Unknown';
449+
450+
const mitigation_title = created + ":" + user_name + ":" + action;
451+
mitigation += mitigation_title + "<br>";
452+
mitigation += "<b>User:</b> " + user_name + "<br>";;
453+
mitigation += "<b>Created:</b> " + created + "<br>";;
454+
mitigation += "<b>Action:</b> " + action + "<br>";;
455+
456+
if(!mitigationStatus.includes(action)){
457+
mitigation += "<b>Technique:</b> " + technique + "<br/>";
458+
mitigation += "<b>Specifics:</b> " + specifics + "<br>";
459+
mitigation += "<b>Remaining Risk:</b> " + remaining_risk + "<br>";
460+
mitigation += "<b>Verification:</b> " + verification + "<br>";
461+
} else {
462+
mitigation += "<b>Comment:</b>" + comment;
463+
}
464+
465+
return { mitigation_title, mitigation }
466+
}
467+
468+
async function checkExistingComments(adoClient, url, workItemId){
469+
try {
470+
const response = await adoClient.get(url, {
471+
headers: {
472+
'Content-Type': 'application/json'
473+
}
474+
});
475+
476+
return response.data.comments
477+
} catch (error) {
478+
console.error(`Failed to get comment for work item ${workItemId}:`, error.message);
479+
throw error;
480+
}
481+
}
482+
483+
async function updateWorkItem(adoClient, adoOrg, adoProject, workItemId, annotations, params) {
484+
const { commit_hash, debug } = params;
485+
const url = `/${adoOrg}/${adoProject}/_apis/wit/workItems/${workItemId}/comments?api-version=7.0-preview.3`;
486+
487+
const sorted_annotations = annotations.sort(function(a, b){
488+
const dateA = new Date(a.created);
489+
const dateB = new Date(b.created);
490+
return dateA - dateB;
491+
})
492+
493+
for(const annot of sorted_annotations){
494+
const { mitigation_title, mitigation } = formatMitigation(annot)
495+
const comments = await checkExistingComments(adoClient, url, workItemId)
496+
497+
let duplicate_comment = comments.find(({text}) => text.startsWith(mitigation_title))
498+
499+
if(duplicate_comment === undefined){
500+
const payload = { text: mitigation }
501+
502+
addComment(adoClient, url, workItemId, payload, debug)
503+
} else {
504+
if(debug === 'true'){
505+
console.log(`Skipping duplicate comment found for work item ${workItemId} with ${mitigation_title}`);
506+
}
507+
}
508+
}
509+
}
510+
511+
async function addComment(adoClient, url, workItemId, payload, debug){
512+
try {
513+
const response = await adoClient.post(url, payload, {
514+
headers: {
515+
'Content-Type': 'application/json'
516+
}
517+
});
518+
console.log('Work item comment added successfully');
519+
if (debug === 'true'){
520+
console.log('Adding Mitigation Comments to work item with payload:', JSON.stringify(payload, null, 2));
521+
}
522+
return response;
523+
} catch (error) {
524+
console.error(`Failed to add comment to work item ${workItemId}:`, error.message);
525+
throw error;
526+
}
527+
}
528+
434529
async function reopenWorkItem(adoClient, adoOrg, adoProject, workItemId, params) {
435530
const { source_base_path_1, source_base_path_2, source_base_path_3, commit_hash, debug } = params;
436531

@@ -439,7 +534,7 @@ async function reopenWorkItem(adoClient, adoOrg, adoProject, workItemId, params)
439534
{
440535
op: 'replace',
441536
path: '/fields/System.State',
442-
value: 'Active'
537+
value: systemState
443538
},
444539
{
445540
op: 'add',
@@ -453,7 +548,11 @@ async function reopenWorkItem(adoClient, adoOrg, adoProject, workItemId, params)
453548
}
454549

455550
try {
456-
const response = await adoClient.patch(url, payload);
551+
const response = await adoClient.patch(url, payload, {
552+
headers: {
553+
'Content-Type': 'application/json-patch+json'
554+
}
555+
});
457556
if (debug === 'true') {
458557
console.log('Work item reopened successfully:', response.data.id);
459558
}
@@ -537,7 +636,11 @@ async function createWorkItem(adoClient, adoOrg, project, workItemType, flaw, pa
537636
}
538637

539638
try {
540-
const response = await adoClient.post(url, payload);
639+
const response = await adoClient.post(url, payload, {
640+
headers: {
641+
'Content-Type': 'application/json-patch+json'
642+
}
643+
});
541644
if (debug === 'true') {
542645
console.log('Response:', JSON.stringify(response.data, null, 2));
543646
}
@@ -669,7 +772,11 @@ async function closeWorkItem(adoClient, adoOrg, adoProject, workItemId, commit_h
669772
}
670773

671774
try {
672-
const response = await adoClient.patch(url, payload);
775+
const response = await adoClient.patch(url, payload, {
776+
headers: {
777+
'Content-Type': 'application/json-patch+json'
778+
}
779+
});
673780
if (debug === 'true') {
674781
console.log('Work item closed successfully:', response.data.id);
675782
}
@@ -680,6 +787,43 @@ async function closeWorkItem(adoClient, adoOrg, adoProject, workItemId, commit_h
680787
}
681788
}
682789

790+
async function closePipelineFlaws(adoClient, adoOrg, adoProject, activeWorkItems, processedFlawIds, commit_hash, debug){
791+
// Close work items that are no longer present in the scan results
792+
console.log(`\nChecking for work items to close (flaws not found in current scan)...`);
793+
794+
for (const workItem of activeWorkItems) {
795+
try {
796+
const title = workItem.fields['System.Title'] || '';
797+
const workItemId = workItem.id;
798+
799+
// Check if this work item corresponds to a flaw that's still present
800+
const isStillPresent = Array.from(processedFlawIds).some(flawId => {
801+
return title.includes(flawId) || isWorkItemMatchingFlaw(workItem, flawId);
802+
});
803+
804+
if (!isStillPresent) {
805+
console.log(`Closing work item ${workItemId} - flaw no longer found in scan: "${title}"`);
806+
await closeWorkItem(adoClient, adoOrg, adoProject, workItemId, flawResolution="CLOSED BY SCAN", commit_hash, debug);
807+
closedCount++;
808+
809+
// Wait between API calls to avoid rate limiting
810+
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
811+
} else {
812+
if (debug === 'true') {
813+
console.log(`Keeping work item ${workItemId} open - flaw still present: "${title}"`);
814+
}
815+
}
816+
} catch (error) {
817+
console.error(`Failed to close work item ${workItem.id}: ${error.message}`);
818+
if (fail_build === 'true') {
819+
throw error;
820+
}
821+
}
822+
}
823+
824+
return closedCount
825+
}
826+
683827
function isWorkItemMatchingFlaw(workItem, flawId) {
684828
const title = workItem.fields['System.Title'] || '';
685829
const tags = workItem.fields['System.Tags'] || '';
@@ -977,6 +1121,9 @@ async function processPolicyFlawsADO(adoPatchClient, adoOrg, adoProject, adoWork
9771121
const flawId = flaw.issue_id || 'Unknown';
9781122
const cweId = flaw.finding_details?.cwe?.id || 'Unknown';
9791123
const cweName = flaw.finding_details?.cwe?.name || 'Unknown';
1124+
const annotations = flaw.annotations || [];
1125+
const flawStatus = flaw.finding_status?.status
1126+
const flawResolution = flaw.finding_status?.resolution
9801127

9811128
// Create a unique identifier for the flaw
9821129
const veracodeFlawId = createVeracodeFlawId(flaw, 'policy');
@@ -1009,6 +1156,7 @@ async function processPolicyFlawsADO(adoPatchClient, adoOrg, adoProject, adoWork
10091156
} else {
10101157
console.log(`Work item ${workItemId} is already open (State: ${workItemState}), skipping creation`);
10111158
skippedCount++;
1159+
10121160
}
10131161
} else {
10141162
// Create new work item
@@ -1044,7 +1192,7 @@ async function processPolicyFlawsADO(adoPatchClient, adoOrg, adoProject, adoWork
10441192
}
10451193
}
10461194

1047-
return { createdCount, reopenedCount, skippedCount };
1195+
return { createdCount, reopenedCount, skippedCount, closedCount };
10481196
}
10491197

10501198
module.exports = {

0 commit comments

Comments
 (0)