diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa05ae..351fcd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.0] - 2026-03-05 +### Added +- Added `scanPath` input to configure a relative path within the repository as the scan root (e.g., `src` or `packages/api`). Defaults to `.` (repository root). +### Changed +- Upgraded scanoss-py version to v1.46.0 + ## [1.6.0] - 2026-02-10 ### Added - Added support for scan tuning parameters @@ -51,4 +57,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.3.0]: https://github.com/scanoss/ado-code-scan/compare/v1.2.0...v1.3.0 [1.4.0]: https://github.com/scanoss/ado-code-scan/compare/v1.3.0...v1.4.0 [1.5.0]: https://github.com/scanoss/ado-code-scan/compare/v1.4.0...v1.5.0 -[1.6.0]: https://github.com/scanoss/ado-code-scan/compare/v1.5.0...v1.6.0 \ No newline at end of file +[1.6.0]: https://github.com/scanoss/ado-code-scan/compare/v1.5.0...v1.6.0 +[1.7.0]: https://github.com/scanoss/ado-code-scan/compare/v1.6.0...v1.7.0 \ No newline at end of file diff --git a/OVERVIEW.md b/OVERVIEW.md index a2f5da2..b73e499 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -169,7 +169,7 @@ When the pipeline is manually triggered or runs on a schedule, the results are u | depTrackProjectVersion | Dependency Track project version (required if projectId not provided). | Optional | - | | apiUrl | SCANOSS API URL | Optional | `https://api.osskb.org/scan/direct` | | apiKey | SCANOSS API Key | Optional | - | -| runtimeContainer | Runtime URL | Optional | `ghcr.io/scanoss/scanoss-py:v1.45.0` | +| runtimeContainer | Runtime URL | Optional | `ghcr.io/scanoss/scanoss-py:v1.46.0` | | licensesCopyleftInclude | List of Copyleft licenses to append to the default list. Provide licenses as a comma-separated list. | Optional | - | | licensesCopyleftExclude | List of Copyleft licenses to remove from default list. Provide licenses as a comma-separated list. | Optional | - | | licensesCopyleftExplicit | Explicit list of Copyleft licenses to consider. Provide licenses as a comma-separated list. | Optional | - | @@ -177,6 +177,7 @@ When the pipeline is manually triggered or runs on a schedule, the results are u | scanFiles | Enable or disable file and snippet scanning | Optional | `true` | | scanossSettings | Settings file to use for scanning. See the SCANOSS settings [documentation](https://scanoss.readthedocs.io/projects/scanoss-py/en/latest/#settings-file) | Optional | `true` | | settingsFilepath | Filepath of the SCANOSS settings to be used for scanning | Optional | `scanoss.json` | +| scanPath | Relative path within the repository to scan (e.g., "src" or "packages/api"). Must be relative, no parent directory references (..) allowed. | Optional | `.` | | debug | Enable debugging | Optional | `false` | | adoPat | ADO Personal Access Token | Optional | - | diff --git a/codescantask/app.input.ts b/codescantask/app.input.ts index 1467579..14eb22b 100644 --- a/codescantask/app.input.ts +++ b/codescantask/app.input.ts @@ -22,6 +22,9 @@ */ import tl = require('azure-pipelines-task-lib/task'); +import { validateScanPath } from './utils/path.utils'; + +export const SCAN_PATH = validateScanPath(tl.getInput('scanPath')); export const DEPENDENCIES_ENABLED = tl.getInput('dependenciesEnabled') === 'true'; export const DEPENDENCIES_SCOPE = tl.getInput('dependenciesScope'); export const DEPENDENCY_SCOPE_EXCLUDE = tl.getInput('dependenciesScopeExclude'); @@ -34,7 +37,7 @@ export const API_URL = tl.getInput('apiUrl'); export const OUTPUT_FILEPATH = tl.getInput('outputFilepath') || "scanoss-raw.json"; export const REPO_DIR = tl.getVariable('Build.Repository.LocalPath') || ''; // Get repository path export const POLICIES_HALT_ON_FAILURE = tl.getInput('policiesHaltOnFailure') === 'true'; -export const RUNTIME_CONTAINER = tl.getInput('runtimeContainer') || "ghcr.io/scanoss/scanoss-py:v1.45.0"; +export const RUNTIME_CONTAINER = tl.getInput('runtimeContainer') || "ghcr.io/scanoss/scanoss-py:v1.46.0"; export const SKIP_SNIPPETS = tl.getInput('skipSnippets') === 'true'; export const SCAN_FILES = tl.getInput('scanFiles') === 'true'; export const SCANOSS_SETTINGS = tl.getInput('scanossSettings') === 'true'; diff --git a/codescantask/package-lock.json b/codescantask/package-lock.json index 74a7219..29a8550 100644 --- a/codescantask/package-lock.json +++ b/codescantask/package-lock.json @@ -1,12 +1,12 @@ { "name": "azure-devops-integration", - "version": "0.21.72", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-devops-integration", - "version": "0.21.72", + "version": "1.7.0", "license": "ISC", "dependencies": { "axios": "^1.7.2", diff --git a/codescantask/package.json b/codescantask/package.json index 88ed0c4..cf5045a 100644 --- a/codescantask/package.json +++ b/codescantask/package.json @@ -1,6 +1,6 @@ { "name": "azure-devops-integration", - "version": "1.6.0", + "version": "1.7.0", "description": "", "main": "index.js", "scripts": { diff --git a/codescantask/services/scan.service.ts b/codescantask/services/scan.service.ts index b3fd3b1..4c9cef6 100644 --- a/codescantask/services/scan.service.ts +++ b/codescantask/services/scan.service.ts @@ -29,7 +29,7 @@ import { DEPENDENCIES_ENABLED, DEPENDENCIES_SCOPE, DEPENDENCY_SCOPE_EXCLUDE, DEPENDENCY_SCOPE_INCLUDE, EXECUTABLE, OUTPUT_FILEPATH, REPO_DIR, RUNTIME_CONTAINER, - SCAN_FILES, SCANOSS_SETTINGS, SETTINGS_FILE_PATH, SKIP_SNIPPETS + SCAN_FILES, SCAN_PATH, SCANOSS_SETTINGS, SETTINGS_FILE_PATH, SKIP_SNIPPETS } from '../app.input'; import { ScannerResults } from './result.interface'; import path from 'path'; @@ -297,7 +297,7 @@ export class ScanService { */ private async buildArgs(): Promise> { return ['run','-v',`${this.options.inputFilepath}:/scanoss`, - this.options.runtimeContainer, 'scan', '.', '--output', `./${OUTPUT_FILEPATH}`, + this.options.runtimeContainer, 'scan', SCAN_PATH, '--output', `./${OUTPUT_FILEPATH}`, ...this.buildDependenciesArgs(), ...await this.detectSBOM(), ...this.buildSnippetArgs(), diff --git a/codescantask/task.json b/codescantask/task.json index b38e0bd..a0076e3 100644 --- a/codescantask/task.json +++ b/codescantask/task.json @@ -9,7 +9,7 @@ "author": "SCANOSS", "version": { "Major": 1, - "Minor": 6, + "Minor": 7, "Patch": 0 }, "instanceNameFormat": "SCANOSS Code Scan", @@ -82,7 +82,7 @@ "name": "runtimeContainer", "type": "string", "label": "Runtime container", - "defaultValue": "ghcr.io/scanoss/scanoss-py:v1.45.0", + "defaultValue": "ghcr.io/scanoss/scanoss-py:v1.46.0", "required": false, "helpMarkDown": "Specify runtime container to perform the scan." }, @@ -198,6 +198,14 @@ "required": false, "helpMarkDown": "Project version (required if using project name)" }, + { + "name": "scanPath", + "type": "string", + "label": "Relative path within the repository to scan (e.g., \"src\" or \"packages/api\"). Must be relative, no parent directory references (..) allowed.", + "defaultValue": ".", + "required": false, + "helpMarkDown": "Relative path within the repository to scan (e.g., \"src\" or \"packages/api\"). Must be relative, no parent directory references (..) allowed." + }, { "name": "debug", "type": "boolean", diff --git a/codescantask/tests/path-utils.test.ts b/codescantask/tests/path-utils.test.ts new file mode 100644 index 0000000..0cf8994 --- /dev/null +++ b/codescantask/tests/path-utils.test.ts @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2026, SCANOSS + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import assert from 'assert'; +import { validateScanPath } from '../utils/path.utils'; + +describe('validateScanPath', function () { + + it('should return "." for undefined input', function () { + assert.strictEqual(validateScanPath(undefined), '.'); + }); + + it('should return "." for empty string input', function () { + assert.strictEqual(validateScanPath(''), '.'); + }); + + it('should accept valid relative paths', function () { + assert.strictEqual(validateScanPath('src'), 'src'); + assert.strictEqual(validateScanPath('packages/api'), 'packages/api'); + assert.strictEqual(validateScanPath('src-v2/api_server'), 'src-v2/api_server'); + }); + + it('should strip leading "./" for consistency', function () { + assert.strictEqual(validateScanPath('./src'), 'src'); + assert.strictEqual(validateScanPath('./packages/api'), 'packages/api'); + }); + + it('should reject absolute Unix paths', function () { + assert.strictEqual(validateScanPath('/etc/passwd'), '.'); + assert.strictEqual(validateScanPath('/usr/local/bin'), '.'); + }); + + it('should reject absolute Windows paths', function () { + assert.strictEqual(validateScanPath('C:\\Windows\\System32'), '.'); + assert.strictEqual(validateScanPath('D:\\Projects'), '.'); + }); + + it('should reject parent directory traversal', function () { + assert.strictEqual(validateScanPath('..'), '.'); + assert.strictEqual(validateScanPath('../../../etc'), '.'); + assert.strictEqual(validateScanPath('src/../../etc'), '.'); + }); + + it('should return "." for "." input', function () { + assert.strictEqual(validateScanPath('.'), '.'); + }); +}); \ No newline at end of file diff --git a/codescantask/utils/path.utils.ts b/codescantask/utils/path.utils.ts new file mode 100644 index 0000000..1e8517b --- /dev/null +++ b/codescantask/utils/path.utils.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2026, SCANOSS + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import path = require('path'); + +/** + * Validates a scan path to prevent directory traversal and ensure safe operations. + * @param scanPath - The scan path to validate + * @returns A safe scan path or defaults to current directory + */ +export function validateScanPath(scanPath: string | undefined): string { + if (!scanPath) { + return '.'; + } + + // Normalize and convert to forward slashes for consistency + const normalizedPath = path.normalize(scanPath).replace(/\\/g, '/'); + + // Reject absolute paths (Unix-style and Windows-style) + const windowsAbsolutePattern = /^[a-zA-Z]:/; + if (path.isAbsolute(scanPath) || windowsAbsolutePattern.test(normalizedPath)) { + console.warn(`Absolute scan paths not allowed: ${scanPath}. Using default: .`); + return '.'; + } + + // Reject directory traversal attempts + if (normalizedPath.includes('..')) { + console.warn(`Invalid scan path detected: "${scanPath}". Using default: .`); + return '.'; + } + + // Remove leading './' for consistency + const cleaned = normalizedPath.startsWith('./') ? normalizedPath.slice(2) : normalizedPath; + + return cleaned || '.'; +} \ No newline at end of file diff --git a/vss-extension-dev.json b/vss-extension-dev.json index 3914e5f..e1491c3 100644 --- a/vss-extension-dev.json +++ b/vss-extension-dev.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "scanoss-code-scan-dev", "name": "SCANOSS Code Scan DEV", - "version": "0.21.72", + "version": "1.7.0", "publisher": "SCANOSS", "public": false, "targets": [ diff --git a/vss-extension.json b/vss-extension.json index a3259d3..f925278 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "id": "scanoss-code-scan", "name": "SCANOSS Code Scan", - "version": "1.6.0", + "version": "1.7.0", "publisher": "SCANOSS", "public": true, "targets": [