Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ node_modules
SCANOSS*.vsix
*.js
.idea
/codescantask/.taskkey
/codescantask/.taskkey

policy-check-copyleft-results.md
policy-check-undeclared-results.md
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Changes...

## [1.3.0] - 2025-11-21
### Changed
- Updated copyleft policy check to read results from generated markdown files (`copyleft-details.md` and `copyleft-summary.md`)
- Updated undeclared policy check to read results from generated markdown files (`undeclared-details.md` and `undeclared-summary.md`)
- Added `instanceNameFormat` to the task.json file
- Set default thread PR comment status to `active`
### Fixed
- Fixed repeated comments for policy checks
- Fixed SCANOSS task version in documentation

## [1.2.0] - 2025-11-7
### Added
Expand All @@ -22,4 +30,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded scanoss-py version to v1.37.1

[1.1.0]: https://github.com/scanoss/ado-code-scan/compare/v1.0.3...v1.1.0
[1.2.0]: https://github.com/scanoss/ado-code-scan/compare/v1.1.0...v1.2.0
[1.2.0]: https://github.com/scanoss/ado-code-scan/compare/v1.1.0...v1.2.0
[1.3.0]: https://github.com/scanoss/ado-code-scan/compare/v1.2.0...v1.3.0
2 changes: 1 addition & 1 deletion codescantask/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azure-devops-integration",
"version": "1.2.0",
"version": "1.3.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
24 changes: 22 additions & 2 deletions codescantask/policies/copyleft-policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
REPO_DIR,
RUNTIME_CONTAINER
} from "../app.input";
import path from "path";
import fs from "fs";

/**
* This class checks if any of the components identified in the scanner results are subject to copyleft licenses.
Expand Down Expand Up @@ -66,11 +68,27 @@ export class CopyleftPolicyCheck extends PolicyCheck {
OUTPUT_FILEPATH,
'--format',
'md',
'--output',
'copyleft-details.md',
'--status',
'copyleft-summary.md',
...this.buildCopyleftArgs(),
...(DEBUG ? ['--debug'] : [])
];
}

private async getDetails(detailsFile: string) {
const detailsPath = path.join(REPO_DIR, detailsFile);
tl.debug(`Reading copyleft details from ${detailsPath}`);
return fs.promises.readFile(detailsPath, 'utf-8');
}

private async getSummary(summaryFile:string) {
const summaryPath = path.join(REPO_DIR, summaryFile);
tl.debug(`Reading copyleft summary from ${summaryPath}`);
return fs.promises.readFile(summaryPath, 'utf-8');
}

async run(): Promise<void> {
await this.start();

Expand All @@ -89,9 +107,11 @@ export class CopyleftPolicyCheck extends PolicyCheck {
tl.setResult(tl.TaskResult.Failed, "Copyleft policy failed: See logs for more details.");
return;
}
const copyLeftDetailsResults = await this.getDetails('copyleft-details.md');
const copyLeftSummaryResults = await this.getSummary('copyleft-summary.md');

await this.uploadArtifact(this.policyCheckResultName, results.stdout);
await this.reject(results.stderr, results.stdout);
await this.uploadArtifact(this.policyCheckResultName, `${copyLeftDetailsResults}\n${copyLeftSummaryResults}`);
await this.reject(copyLeftSummaryResults, copyLeftDetailsResults);

}

Expand Down
86 changes: 77 additions & 9 deletions codescantask/policies/policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,23 @@ export abstract class PolicyCheck {
tl.setResult(status, `[${this.checkName}], SUMMARY: ${summary}, DETAILS: ${text? text: ''} `);
await this.updatePRStatus(PR_STATUS.failed, `SCANOSS Policy Check: ${this.checkName}`);
if (text) {
await this.addCommentToPR(`${this.checkName} Check Results`, text);
await this.addCommentToPR(`${this.checkName} Results`, text);
}

}

protected async updatePRStatus(state: PR_STATUS, description: string){
if (this.buildReason && this.buildReason !== 'PullRequest') return;
try {
if (!this.accessToken || !this.orgUrl || !this.project || !this.repositoryId || !this.pullRequestId) {
const commitId = tl.getVariable('System.PullRequest.SourceCommitId');

if (!this.accessToken || !this.orgUrl || !this.project || !this.repositoryId || !commitId) {
tl.setResult(tl.TaskResult.SucceededWithIssues, `Missing necessary environment variables.\n
Access Token: ${this.accessToken}\n
Organization url: ${this.orgUrl}\n
Project: ${this.project}\n
Repository ID: ${this.repositoryId}\n
Pull request id: ${this.repositoryId}
Commit ID: ${commitId}
`);
return;
}
Expand All @@ -98,29 +100,80 @@ export abstract class PolicyCheck {
},
};

// Post the status to the PR
const apiUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/statuses?api-version=6.0-preview.1`;
// Post the status to the commit
const apiUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/commits/${commitId}/statuses?api-version=7.1`;
await axios.post(apiUrl, status, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
}
});
} catch (err:any) {
tl.setResult(tl.TaskResult.SucceededWithIssues, `Failed to add status to PR: ${err.message}`);
tl.setResult(tl.TaskResult.SucceededWithIssues, `Failed to add status to commit: ${err.message}`);
}
}

protected async addCommentToPR(title: string, content: string) {
/**
* Deletes existing SCANOSS comments for this check type from the PR
* Identifies comments by the SCANOSS marker and check name in the content
*/
private async deletePreviousComments(title: string):Promise<void> {
if (this.buildReason && this.buildReason !== 'PullRequest') return;
try {
const apiUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads?api-version=6.0`;

const response = await axios.get(apiUrl, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
}
});

const threads = response.data.value || [];
const scanossMarker = `SCANOSS - ${title}`;
tl.debug(`Looking for threads with marker: ${scanossMarker}`);
for (const thread of threads) {
if (thread.comments && thread.comments.length > 0) {
const firstComment = thread.comments[0];
tl.debug(`Thread ${thread.id} - First comment content: ${firstComment.content?.substring(0, 100)}...`);

// Check if this is a SCANOSS comment for this check type
if (firstComment.content && firstComment.content.includes(scanossMarker)) {
tl.debug(`Found SCANOSS comment thread ${thread.id}, deleting all comments`);
// Delete all comments in this thread
for (const comment of thread.comments) {
const deleteUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads/${thread.id}/comments/${comment.id}?api-version=7.1`;
await axios.delete(deleteUrl, {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
tl.debug(`Deleted comment ${comment.id} from thread ${thread.id}`);
}
}
}
}
} catch (error: any) {
tl.warning(`Failed to delete previous comments: ${error.message}`);
}
}

protected async addCommentToPR(title: string, content: string, threadStatus: string = 'pending') {
if (this.buildReason && this.buildReason !== 'PullRequest') return;
try {
// Delete previous comments for this check type
await this.deletePreviousComments(title);

const apiUrl =`${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads?api-version=6.0`;

// Add a hidden marker to identify SCANOSS comments and the check type
const scanossMarker = `SCANOSS - ${title}`;
const payload = {
comments: [{
parentCommentId: 0,
content: `## ${title}\n${content}`
}]
content: `##${scanossMarker}\n\n${content}`
}],
status: threadStatus // Set thread status: active, pending, fixed, wontFix, closed, byDesign, unknown
};

const response = await axios.post(apiUrl, payload, {
Expand All @@ -131,6 +184,21 @@ export abstract class PolicyCheck {
});

tl.debug(`Comment added successfully: ${response.data}`);

// Update the thread status using PATCH endpoint
const threadId = response.data.id;
if (threadId) {
const patchUrl = `${this.orgUrl}${this.project}/_apis/git/repositories/${this.repositoryId}/pullRequests/${this.pullRequestId}/threads/${threadId}?api-version=7.1`;
await axios.patch(patchUrl, {
status: threadStatus
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
}
});
tl.debug(`Thread ${threadId} status updated to: ${threadStatus}`);
}
} catch (error: any) {
tl.error('Failed to add comment:', error.response.data);
}
Expand Down
30 changes: 24 additions & 6 deletions codescantask/policies/undeclared-policy-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import { PolicyCheck } from './policy-check';
import {DEBUG, EXECUTABLE, OUTPUT_FILEPATH, REPO_DIR, RUNTIME_CONTAINER, SCANOSS_SETTINGS} from '../app.input';
import * as tl from 'azure-pipelines-task-lib';
import * as fs from 'fs';
import * as path from 'path';

/**
* Verifies that all components identified in scanner results are declared in the project's SBOM.
Expand All @@ -41,14 +43,28 @@ export class UndeclaredPolicyCheck extends PolicyCheck {

private buildArgs(): Array<string> {
return ['run', '-v', `${REPO_DIR}:/scanoss`, RUNTIME_CONTAINER, 'inspect', 'undeclared', '--input',
OUTPUT_FILEPATH, '--format', 'md',
OUTPUT_FILEPATH,
'--format',
'md',
'--output',
'undeclared-details.md',
'--status',
'undeclared-summary.md',
...(!SCANOSS_SETTINGS ? ['--sbom-format', 'legacy']: []), // Sets sbom format output to legacy if SCANOSS_SETTINGS is not true
...(DEBUG ? ['--debug'] : [])
];
}

private getResults(details: string, summary:string) {
return `${details}\n${summary}`;
private async getDetails(detailsFile: string) {
const detailsPath = path.join(REPO_DIR, detailsFile);
tl.debug(`Reading copyleft details from ${detailsPath}`);
return fs.promises.readFile(detailsPath, 'utf-8');
}

private async getSummary(summaryFile:string) {
const summaryPath = path.join(REPO_DIR, summaryFile);
tl.debug(`Reading copyleft summary from ${summaryPath}`);
return fs.promises.readFile(summaryPath, 'utf-8');
}

async run(): Promise<void> {
Expand All @@ -70,8 +86,10 @@ export class UndeclaredPolicyCheck extends PolicyCheck {
return;
}

const undeclaredComponentsResults = this.getResults(results.stdout,results.stderr);
await this.uploadArtifact(this.policyCheckResultName, undeclaredComponentsResults);
await this.reject(results.stderr, undeclaredComponentsResults);
const detailsResults = await this.getDetails('undeclared-details.md');
const summaryResults = await this.getSummary('undeclared-summary.md');
const combinedResults = `${detailsResults}\n${summaryResults}`;
await this.uploadArtifact(this.policyCheckResultName, combinedResults);
await this.reject(results.stderr, combinedResults);
}
}
25 changes: 0 additions & 25 deletions codescantask/policy-check-undeclared-results.md

This file was deleted.

3 changes: 2 additions & 1 deletion codescantask/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
"author": "SCANOSS",
"version": {
"Major": 1,
"Minor": 2,
"Minor": 3,
"Patch": 0
},
"instanceNameFormat": "SCANOSS Code Scan",
"inputs": [
{
"name": "dependenciesEnabled",
Expand Down
12 changes: 10 additions & 2 deletions codescantask/tests/copyleftPolicySuite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ describe('CopyleftPolicyCheck', () => {
'--input',
'results.json',
'--format',
'md'
'md',
'--output',
'copyleft-details.md',
'--status',
'copyleft-summary.md'
]);
});

Expand Down Expand Up @@ -230,7 +234,11 @@ describe('CopyleftPolicyCheck', () => {
'--input',
'results.json',
'--format',
'md'
'md',
'--output',
'copyleft-details.md',
'--status',
'copyleft-summary.md'
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
| - | :-: | - | - |
| pkg:npm/%40grpc/grpc-js | Apache-2.0 | https://spdx.org/licenses/Apache-2.0.html | YES |
| pkg:npm/abort-controller | MIT | https://spdx.org/licenses/MIT.html | YES |
| pkg:npm/adm-zip | MIT | https://spdx.org/licenses/MIT.html | YES |
| pkg:npm/adm-zip | MIT | https://spdx.org/licenses/MIT.html | YES |
1 change: 1 addition & 0 deletions codescantask/tests/data/copyleft-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3 component(s) with copyleft licenses were found.
3 changes: 3 additions & 0 deletions codescantask/tests/data/undeclared-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Undeclared components
| Component | License |
| - | - |
1 change: 1 addition & 0 deletions codescantask/tests/data/undeclared-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0 undeclared component(s) were found.
Loading