From 010b9a3b7ef1bdca066aa75b203c0db523d6f053 Mon Sep 17 00:00:00 2001 From: Agustin Groh <77737320+agustingroh@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:02:16 -0300 Subject: [PATCH] Feat/groh/integrate scanoss json * feat:SP-1918 Use scanoss-py policies * feat: SP-1919 Add skip snippet and scanoss setting flags * chore: SP-1920 Use scanoss-py copyleft policy * chore: SP-1922 Integrates undeclared components policy from scanoss-py * chore: SP-1924 Adds copyleft policy unit tests * chore: SP-1927 Adds undeclared component unit tests * chore: SP-1964 Removes sbom.json feature * chore: SP-1957 Upgrades scanoss.py version to v1.18.1 * chore: Fixes linter warnings * chore: SP-1979 Removes sbom.json export format * chore: SP-1980 Add warning when SCANOSS settings is not enabled * chore: SP-1977 Adds breaking change documentation v1.0.1 * chore: SP-1978 Upgrades default runtime container version to v1.19.0 --- .eslintignore | 1 + .github/linters/.eslintrc.yml | 4 +- .github/workflows/linter.yml | 4 +- README.md | 79 ++- __tests__/copyleft-argument-builder.test.ts | 116 ++++ __tests__/copyleft-policy-check.test.ts | 112 ++- __tests__/data/empty-results.json | 50 ++ __tests__/data/package.json | 28 + __tests__/data/results.json | 225 ++++++ __tests__/sbom.mock.ts | 20 - __tests__/scan-service.test.ts | 135 ++++ __tests__/undeclared-argument-builder.test.ts | 57 ++ __tests__/undeclared-policy-check.test.ts | 50 +- action.yml | 30 +- dist/index.js | 652 ++++++++++++------ package-lock.json | 4 +- package.json | 2 +- policy-check-undeclared-results.md | 25 + src/app.input.ts | 10 +- src/main.ts | 2 +- .../argument_builders/argument-builder.ts} | 12 +- .../copyleft-argument-builder.ts | 70 ++ .../undeclared-argument-builder.ts | 42 ++ src/policies/copyleft-policy-check.ts | 106 +-- src/policies/policy-check.ts | 3 +- src/policies/undeclared-policy-check.ts | 93 +-- src/services/scan.service.ts | 255 +++++-- 27 files changed, 1657 insertions(+), 530 deletions(-) create mode 100644 __tests__/copyleft-argument-builder.test.ts create mode 100644 __tests__/data/empty-results.json create mode 100644 __tests__/data/package.json create mode 100644 __tests__/data/results.json delete mode 100644 __tests__/sbom.mock.ts create mode 100644 __tests__/scan-service.test.ts create mode 100644 __tests__/undeclared-argument-builder.test.ts create mode 100644 policy-check-undeclared-results.md rename src/{utils/sbom.utils.ts => policies/argument_builders/argument-builder.ts} (83%) create mode 100644 src/policies/argument_builders/copyleft-argument-builder.ts create mode 100644 src/policies/argument_builders/undeclared-argument-builder.ts diff --git a/.eslintignore b/.eslintignore index 9ff5c1c..8e1d2ba 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ lib/ dist/ node_modules/ coverage/ + diff --git a/.github/linters/.eslintrc.yml b/.github/linters/.eslintrc.yml index 3dcfe2f..983c1a3 100644 --- a/.github/linters/.eslintrc.yml +++ b/.github/linters/.eslintrc.yml @@ -57,7 +57,7 @@ rules: '@typescript-eslint/func-call-spacing': ['error', 'never'], '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-empty-interface': 'error', - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-for-in-array': 'error', '@typescript-eslint/no-inferrable-types': 'error', @@ -82,5 +82,5 @@ rules: '@typescript-eslint/type-annotation-spacing': 'error', '@typescript-eslint/unbound-method': 'error', 'github/array-foreach' : 'off', - 'eslint-comments/no-unlimited-disable': 'off' + 'eslint-comments/no-unlimited-disable': 'off', } diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 1532f10..0606233 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -14,7 +14,7 @@ permissions: jobs: lint: name: Lint Codebase - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout @@ -45,3 +45,5 @@ jobs: VALIDATE_JSCPD: false VALIDATE_MARKDOWN: false VALIDATE_NATURAL_LANGUAGE: false + SUPPRESS_FILE_TYPE_WARN: true + VALIDATE_JSON: false diff --git a/README.md b/README.md index 098a4c7..92f8316 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,42 @@ vulnerabilities and license compliance with configurable policies. +## Breaking change v1.0.1 + +- Default runtime container updated to `ghcr.io/scanoss/scanoss-py:v1.19.0` +- Removed parameters: + - `sbom.enabled` + - `sbom.filepath` + - `sbom.type` + +### Converting from sbom.json to scanoss.json +The SBOM configuration format has changed and the file name must be updated from **sbom.json** to **scanoss.json**. Here's how to convert your existing configuration: + +Old format (sbom.json): +```json +{ + "components": [ + { + "purl": "pkg:github/scanoss/scanner.c" + } + ] +} +``` + +New format (scanoss.json): +```json +{ + "bom": { + "include": [ + { + "purl": "pkg:github/scanoss/scanner.c" + } + ] + } +} +``` + + ## Usage To begin using this action, you'll need to set up a basic GitHub workflow and define a job within it: @@ -53,24 +89,25 @@ For example workflow runs, check out our ### Action Input Parameters -| **Parameter** | **Description** | **Required** | **Default** | -|----------------------------|------------------------------------------------------------------------------------------------------|--------------|---------------------------------------| -| output.filepath | Scan output file name. | Optional | `results.json` | -| sbom.enabled | Enable or disable scanning based on the SBOM file | Optional | `true` | -| sbom.filepath | Filepath of the SBOM file to be used for scanning | Optional | `sbom.json` | -| sbom.type | Type of SBOM operation: either 'identify' or 'ignore | Optional | `identify` | -| dependencies.enabled | Option to enable or disable scanning of dependencies. | Optional | `false` | -| dependencies.scope | Gets development or production dependencies (scopes: prod - dev) | Optional | - | -| dependencies.scope.include | Custom list of dependency scopes to be included. Provide scopes as a comma-separated list. | Optional | - | -| dependencies.scope.exclude | Custom list of dependency scopes to be excluded. Provide scopes as a comma-separated list. | Optional | - | -| policies | List of policies separated by commas, options available are: copyleft, undeclared. | Optional | - | -| policies.halt_on_failure | Halt check on policy failure. If set to false checks will not fail. | Optional | `true` | -| api.url | SCANOSS API URL | Optional | `https://osskb.org/api/scan/direct` | -| api.key | SCANOSS API Key | Optional | - | -| licenses.copyleft.include | List of Copyleft licenses to append to the default list. Provide licenses as a comma-separated list. | Optional | - | -| licenses.copyleft.exclude | List of Copyleft licenses to remove from default list. Provide licenses as a comma-separated list. | Optional | - | -| licenses.copyleft.explicit | Explicit list of Copyleft licenses to consider. Provide licenses as a comma-separated list. | Optional | - | -| runtimeContainer | Runtime URL | Optional | `ghcr.io/scanoss/scanoss-py:v1.15.0` | +| **Parameter** | **Description** | **Required** | **Default** | +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|--------------------------------------| +| output.filepath | Scan output file name. | Optional | `results.json` | +| dependencies.enabled | Option to enable or disable scanning of dependencies. | Optional | `false` | +| dependencies.scope | Gets development or production dependencies (scopes: prod - dev) | Optional | - | +| dependencies.scope.include | Custom list of dependency scopes to be included. Provide scopes as a comma-separated list. | Optional | - | +| dependencies.scope.exclude | Custom list of dependency scopes to be excluded. Provide scopes as a comma-separated list. | Optional | - | +| policies | List of policies separated by commas, options available are: copyleft, undeclared. | Optional | - | +| policies.halt_on_failure | Halt check on policy failure. If set to false checks will not fail. | Optional | `true` | +| api.url | SCANOSS API URL | Optional | `https://osskb.org/api/scan/direct` | +| api.key | SCANOSS API Key | Optional | - | +| licenses.copyleft.include | List of Copyleft licenses to append to the default list. Provide licenses as a comma-separated list. | Optional | - | +| licenses.copyleft.exclude | List of Copyleft licenses to remove from default list. Provide licenses as a comma-separated list. | Optional | - | +| licenses.copyleft.explicit | Explicit list of Copyleft licenses to consider. Provide licenses as a comma-separated list. | Optional | - | +| runtimeContainer | Runtime URL | Optional | `ghcr.io/scanoss/scanoss-py:v1.19.0` | +| skipSnippets | Skip the generation of snippets. (scanFiles option must be enabled) | Optional | `false` | +| 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` | ### Action Output Parameters @@ -88,8 +125,8 @@ The SCANOSS Code Scan Action includes two configurable policies: 1. Copyleft: This policy checks if any component or code snippet is associated with a copyleft license. If such a license is detected, the pull request (PR) is rejected. The default list of Copyleft licenses is defined in the following [file](https://github.com/scanoss/gha-code-scan/blob/main/src/utils/license.utils.ts). -2. Undeclared: This policy compares the components detected in the repository against those declared in an sbom.json - file (customizable through the sbom.filepath parameter). If there are undeclared components, the PR is rejected. +2. Undeclared: This policy compares the components detected in the repository against those declared in scanoss.json + file (customizable through the settingsFilepath parameter). If there are undeclared components, the PR is rejected. In this scenario, a classic policy is executed that will fail if copyleft licenses are found within the results: @@ -129,7 +166,7 @@ jobs: id: scanoss-code-scan-step uses: scanoss/code-scan-action@v0 with: - policies: copyleft, undeclared #NOTE: undeclared policy requires a sbom.json in the project root + policies: copyleft, undeclared dependencies.enabled: true # api-url: # api-key: diff --git a/__tests__/copyleft-argument-builder.test.ts b/__tests__/copyleft-argument-builder.test.ts new file mode 100644 index 0000000..13dd605 --- /dev/null +++ b/__tests__/copyleft-argument-builder.test.ts @@ -0,0 +1,116 @@ +import { CopyLeftArgumentBuilder } from '../src/policies/argument_builders/copyleft-argument-builder'; +import { RUNTIME_CONTAINER } from '../src/app.input'; + +jest.mock('../src/app.input', () => ({ + ...jest.requireActual('../src/app.input'), + REPO_DIR: 'scanoss', + OUTPUT_FILEPATH: 'results.json', + COPYLEFT_LICENSE_EXCLUDE: '', + COPYLEFT_LICENSE_EXPLICIT: '', + COPYLEFT_LICENSE_INCLUDE: '' +})); +describe('CopyleftArgumentBuilder', () => { + // Store the module for direct manipulation + const appInput = jest.requireMock('../src/app.input'); + + afterEach(() => { + appInput.COPYLEFT_LICENSE_EXPLICIT = ''; + appInput.COPYLEFT_LICENSE_EXCLUDE = ''; + appInput.COPYLEFT_LICENSE_INCLUDE = ''; + }); + + it('Copyleft explicit test', async () => { + appInput.COPYLEFT_LICENSE_EXPLICIT = 'MIT,Apache-2.0'; + appInput.COPYLEFT_LICENSE_EXCLUDE = 'MIT,Apache-2.0'; + const builder = new CopyLeftArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'scanoss:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + 'results.json', + '--format', + 'md', + '--explicit', + 'MIT,Apache-2.0' + ]); + }); + + it('Copyleft exclude test', async () => { + appInput.COPYLEFT_LICENSE_EXCLUDE = 'MIT,Apache-2.0'; + const builder = new CopyLeftArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'scanoss:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + 'results.json', + '--format', + 'md', + '--exclude', + 'MIT,Apache-2.0' + ]); + }); + + it('Copyleft include test', async () => { + appInput.COPYLEFT_LICENSE_INCLUDE = 'MIT,Apache-2.0,LGPL-3.0-only'; + const builder = new CopyLeftArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'scanoss:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + 'results.json', + '--format', + 'md', + '--include', + 'MIT,Apache-2.0,LGPL-3.0-only' + ]); + }); + + it('Copyleft empty parameters test', async () => { + const builder = new CopyLeftArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'scanoss:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + 'results.json', + '--format', + 'md' + ]); + }); + + it('Build Command test', async () => { + const builder = new CopyLeftArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'scanoss:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + 'results.json', + '--format', + 'md' + ]); + }); +}); diff --git a/__tests__/copyleft-policy-check.test.ts b/__tests__/copyleft-policy-check.test.ts index a5916ab..05111ea 100644 --- a/__tests__/copyleft-policy-check.test.ts +++ b/__tests__/copyleft-policy-check.test.ts @@ -1,7 +1,15 @@ +import path from 'path'; import { CopyleftPolicyCheck } from '../src/policies/copyleft-policy-check'; -import { CONCLUSION, PolicyCheck } from '../src/policies/policy-check'; -import { ScannerResults } from '../src/services/result.interfaces'; -import { resultsMock } from './results.mock'; +import { CONCLUSION } from '../src/policies/policy-check'; + +jest.mock('../src/app.input', () => ({ + ...jest.requireActual('../src/app.input'), + REPO_DIR: '', + OUTPUT_FILEPATH: 'results.json', + COPYLEFT_LICENSE_EXCLUDE: '', + COPYLEFT_LICENSE_EXPLICIT: '', + COPYLEFT_LICENSE_INCLUDE: '' +})); // Mock the @actions/github module jest.mock('@actions/github', () => ({ @@ -14,49 +22,85 @@ jest.mock('@actions/github', () => ({ getOctokit: jest.fn().mockReturnValue({ rest: { checks: { - update: jest.fn().mockResolvedValue({}) + update: jest.fn().mockResolvedValue({}), + create: jest.fn().mockReturnValue({ + data: { + id: 1 + } + }) } } }) })); describe('CopyleftPolicyCheck', () => { - let scannerResults: ScannerResults; - let policyCheck: CopyleftPolicyCheck; + const appInput = jest.requireMock('../src/app.input'); + + afterEach(() => { + appInput.COPYLEFT_LICENSE_EXPLICIT = ''; + appInput.COPYLEFT_LICENSE_EXCLUDE = ''; + appInput.COPYLEFT_LICENSE_INCLUDE = ''; + }); - beforeEach(() => { - jest.clearAllMocks(); + it('Copyleft policy check fail', async () => { + const TEST_DIR = __dirname; + const TEST_REPO_DIR = path.join(TEST_DIR, 'data'); + const TEST_RESULTS_FILE = 'results.json'; - policyCheck = new CopyleftPolicyCheck(); - jest.spyOn(PolicyCheck.prototype, 'uploadArtifact').mockImplementation(async () => { + appInput.REPO_DIR = TEST_REPO_DIR; + appInput.OUTPUT_FILEPATH = TEST_RESULTS_FILE; + + jest.spyOn(CopyleftPolicyCheck.prototype, 'uploadArtifact').mockImplementation(async () => { return Promise.resolve({ id: 123456 }); }); - jest.spyOn(PolicyCheck.prototype, 'initStatus').mockImplementation(); - jest.spyOn(PolicyCheck.prototype, 'finish').mockImplementation(); - }); + jest.spyOn(CopyleftPolicyCheck.prototype, 'initStatus').mockImplementation(); + jest.spyOn(CopyleftPolicyCheck.prototype, 'updateCheck').mockImplementation(); + const copyleftPolicyCheck = new CopyleftPolicyCheck(); + await copyleftPolicyCheck.start(1); + await copyleftPolicyCheck.run(); + //neutral cause policy policy halt on failure is not set + expect(copyleftPolicyCheck.conclusion).toEqual(CONCLUSION.Neutral); + }, 50000); - it('should pass the policy check when no copyleft components are found', async () => { - scannerResults = JSON.parse(resultsMock[0].content); - await policyCheck.run(scannerResults); - expect(policyCheck.conclusion).toEqual(CONCLUSION.Success); - }); + it('Copyleft policy empty results', async () => { + const TEST_DIR = __dirname; + const TEST_REPO_DIR = path.join(TEST_DIR, 'data'); + const TEST_RESULTS_FILE = 'results.json'; - it('should fail the policy check when copyleft components are found', async () => { - scannerResults = JSON.parse(resultsMock[2].content); - await policyCheck.run(scannerResults); - expect(policyCheck.conclusion).toEqual(CONCLUSION.Neutral); - }); + appInput.REPO_DIR = TEST_REPO_DIR; + appInput.OUTPUT_FILEPATH = TEST_RESULTS_FILE; + appInput.COPYLEFT_LICENSE_EXCLUDE = 'GPL-2.0-only'; - it('should fail the policy check when copyleft dependencies are found', async () => { - scannerResults = JSON.parse(resultsMock[4].content); - await policyCheck.run(scannerResults); - // NEUTRAL is the same as failure in this context. See inputs.POLICIES_HALT_ON_FAILURE. (Default FALSE) - expect(policyCheck.conclusion).toEqual(CONCLUSION.Neutral); - }); + jest.spyOn(CopyleftPolicyCheck.prototype, 'uploadArtifact').mockImplementation(async () => { + return Promise.resolve({ id: 123456 }); + }); + jest.spyOn(CopyleftPolicyCheck.prototype, 'initStatus').mockImplementation(); + jest.spyOn(CopyleftPolicyCheck.prototype, 'updateCheck').mockImplementation(); + const copyleftPolicyCheck = new CopyleftPolicyCheck(); + await copyleftPolicyCheck.start(1); + await copyleftPolicyCheck.run(); + //neutral cause policy policy halt on failure is not set + expect(copyleftPolicyCheck.conclusion).toEqual(CONCLUSION.Success); + }, 50000); - it('should pass the copyleft policy check', async () => { - scannerResults = JSON.parse(resultsMock[5].content); - await policyCheck.run(scannerResults); - expect(policyCheck.conclusion).toEqual(CONCLUSION.Success); - }); + it('Copyleft policy explicit licenses', async () => { + const TEST_DIR = __dirname; + const TEST_REPO_DIR = path.join(TEST_DIR, 'data'); + const TEST_RESULTS_FILE = 'results.json'; + + appInput.REPO_DIR = TEST_REPO_DIR; + appInput.OUTPUT_FILEPATH = TEST_RESULTS_FILE; + appInput.COPYLEFT_LICENSE_EXPLICIT = 'MIT,Apache-2.0'; + + jest.spyOn(CopyleftPolicyCheck.prototype, 'uploadArtifact').mockImplementation(async () => { + return Promise.resolve({ id: 123456 }); + }); + jest.spyOn(CopyleftPolicyCheck.prototype, 'initStatus').mockImplementation(); + jest.spyOn(CopyleftPolicyCheck.prototype, 'updateCheck').mockImplementation(); + const copyleftPolicyCheck = new CopyleftPolicyCheck(); + await copyleftPolicyCheck.start(1); + await copyleftPolicyCheck.run(); + //neutral cause policy policy halt on failure is not set + expect(copyleftPolicyCheck.conclusion).toEqual(CONCLUSION.Neutral); + }, 30000); }); diff --git a/__tests__/data/empty-results.json b/__tests__/data/empty-results.json new file mode 100644 index 0000000..cd352a0 --- /dev/null +++ b/__tests__/data/empty-results.json @@ -0,0 +1,50 @@ +{ + "crc32c.c": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + } + } + ], + "json.c": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + } + } + ], + "log.c": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + } + } + ], + "package.json": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + } + } + ] +} diff --git a/__tests__/data/package.json b/__tests__/data/package.json new file mode 100644 index 0000000..77df647 --- /dev/null +++ b/__tests__/data/package.json @@ -0,0 +1,28 @@ +{ + "name": "scanoss", + "version": "0.15.3", + "description": "The SCANOSS JS package provides a simple, easy to consume module for interacting with SCANOSS APIs/Engine.", + "main": "build/main/index.js", + "typings": "build/main/index.d.ts", + "module": "build/module/index.js", + "repository": "https://github.com/scanoss/scanoss.js", + "license": "MIT", + "keywords": [], + "bin": { + "scanoss-js": "build/main/cli/bin/cli-bin.js" + }, + "scripts": { + "build": "run-p build:*", + "build:main": "tsc -p tsconfig.json", + "build:module": "tsc -p tsconfig.module.json", + "test": "nyc mocha -r ts-node/register 'tests/**/*.ts' 'src/**/*.spec.ts'", + "install-dev": "npm run build && npm run test && npm install -g ." + }, + "engines": { + "node": ">=10" + }, + "dependencies": { + "@grpc/grpc-js": "^1.5.5", + "abort-controller": "^3.0.0" + } +} diff --git a/__tests__/data/results.json b/__tests__/data/results.json new file mode 100644 index 0000000..a3da3a6 --- /dev/null +++ b/__tests__/data/results.json @@ -0,0 +1,225 @@ +{ + "crc32c.c": [ + { + "component": "wfp", + "file": "wfp-6afc1f6163d1d6c8d03ff5211a0571118e08da1f/src/external/crc32c/crc32c.c", + "file_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "file_url": "https://api.osskb.org/file_contents/0fe279946d388ef07d9c3f6e3ffb8ebe", + "id": "file", + "latest": "0ed473d", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/Zlib.txt", + "copyleft": "no", + "name": "Zlib", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "no", + "source": "scancode", + "url": "https://spdx.org/licenses/Zlib.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/Zlib.txt", + "copyleft": "no", + "name": "Zlib", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "no", + "source": "file_header", + "url": "https://spdx.org/licenses/Zlib.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "yes", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "yes", + "source": "license_file", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/wfp" + ], + "release_date": "2020-07-12", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + }, + "source_hash": "0fe279946d388ef07d9c3f6e3ffb8ebe", + "status": "pending", + "url": "https://github.com/scanoss/wfp", + "url_hash": "9b36f30d422d7f77854f298f63c55256", + "url_stats": {}, + "vendor": "scanoss", + "version": "6afc1f6" + } + ], + "json.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/json.c", + "file_hash": "8e4d433c1547b59681379e9fe9960546", + "file_url": "https://api.osskb.org/file_contents/8e4d433c1547b59681379e9fe9960546", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt", + "copyleft": "no", + "name": "BSD-2-Clause", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "no", + "source": "file_header", + "url": "https://spdx.org/licenses/BSD-2-Clause.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + }, + "source_hash": "8e4d433c1547b59681379e9fe9960546", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "log.c": [ + { + "component": "scanner.c", + "file": "scanner.c-1.3.3/external/src/log.c", + "file_hash": "f00c8a010806ff1593b15c7cbff7e594", + "file_url": "https://api.osskb.org/file_contents/f00c8a010806ff1593b15c7cbff7e594", + "id": "file", + "latest": "1.3.4", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1", + "name": "GPL-2.0-only", + "osadl_updated": "2024-11-04T13:45:00+0000", + "patent_hints": "yes", + "source": "component_declared", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/scanoss/scanner.c" + ], + "release_date": "2021-05-26", + "server": { + "kb_version": { + "daily": "24.11.12", + "monthly": "24.10" + }, + "version": "5.4.8" + }, + "source_hash": "f00c8a010806ff1593b15c7cbff7e594", + "status": "pending", + "url": "https://github.com/scanoss/scanner.c", + "url_hash": "2d1700ba496453d779d4987255feb5f2", + "url_stats": {}, + "vendor": "scanoss", + "version": "1.3.3" + } + ], + "package.json": [ + { + "dependencies": [ + { + "component": "@grpc/grpc-js", + "licenses": [ + { + "is_spdx_approved": true, + "name": "Apache-2.0", + "spdx_id": "Apache-2.0" + } + ], + "purl": "pkg:npm/%40grpc/grpc-js", + "url": "https://www.npmjs.com/package/%40grpc/grpc-js", + "version": "1.12.2" + }, + { + "component": "abort-controller", + "licenses": [ + { + "is_spdx_approved": true, + "name": "MIT", + "spdx_id": "MIT" + } + ], + "purl": "pkg:npm/abort-controller", + "url": "https://www.npmjs.com/package/abort-controller", + "version": "3.0.0" + }, + { + "component": "adm-zip", + "licenses": [ + { + "is_spdx_approved": true, + "name": "MIT", + "spdx_id": "MIT" + } + ], + "purl": "pkg:npm/adm-zip", + "url": "https://www.npmjs.com/package/adm-zip", + "version": "0.5.16" + } + ], + "id": "dependency", + "status": "pending" + } + ] +} diff --git a/__tests__/sbom.mock.ts b/__tests__/sbom.mock.ts deleted file mode 100644 index 00725a9..0000000 --- a/__tests__/sbom.mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SBOM } from '../src/utils/sbom.utils'; - -export const sbomMock: SBOM[] = [ - { - components: [] // empty SBOM - }, - { - components: [ - { purl: 'pkg:github/scanoss/engine' }, - { purl: 'pkg:github/scanoss/engine' }, - { purl: 'pkg:github/scanoss/engine' }, - { purl: 'pkg:pypi/requests' }, - { purl: 'pkg:pypi/crc32c' }, - { purl: 'pkg:pypi/binaryornot' }, - { purl: 'pkg:pypi/pytest' }, - { purl: 'pkg:pypi/pytest-cov' }, - { purl: 'pkg:pypi/beautifulsoup4' } - ] - } -]; diff --git a/__tests__/scan-service.test.ts b/__tests__/scan-service.test.ts new file mode 100644 index 0000000..ba3d019 --- /dev/null +++ b/__tests__/scan-service.test.ts @@ -0,0 +1,135 @@ +import { RUNTIME_CONTAINER } from '../src/app.input'; +import { ScanService } from '../src/services/scan.service'; +import fs from 'fs'; +import path from 'path'; + +jest.mock('../src/app.input', () => ({ + ...jest.requireActual('../src/app.input'), + REPO_DIR: '', + OUTPUT_FILEPATH: 'results.json', + COPYLEFT_LICENSE_EXCLUDE: '', + COPYLEFT_LICENSE_EXPLICIT: '', + COPYLEFT_LICENSE_INCLUDE: '' +})); + +describe('ScanService', () => { + const appInput = jest.requireMock('../src/app.input'); + it('should correctly return the dependency scope command', () => { + const service = new ScanService({ + outputFilepath: '', + inputFilepath: '', + runtimeContainer: RUNTIME_CONTAINER, + dependencyScope: 'prod', + dependencyScopeInclude: '', + dependencyScopeExclude: '', + scanFiles: true, + skipSnippets: false, + settingsFilePath: '', + scanossSettings: false + }); + + // Accessing the private method by bypassing TypeScript type checks + const command = (service as any).dependencyScopeArgs(); + console.log(command); + expect(command).toEqual(['--dep-scope', 'prod']); + }); + + it('Should return --dependencies-only parameter', () => { + const service = new ScanService({ + outputFilepath: '', + inputFilepath: '', + runtimeContainer: RUNTIME_CONTAINER, + dependencyScope: '', + dependencyScopeInclude: '', + dependencyScopeExclude: '', + dependenciesEnabled: true, + scanFiles: false, + skipSnippets: false, + settingsFilePath: '', + scanossSettings: false + }); + + const command = (service as any).buildDependenciesArgs(); + expect(command).toEqual(['--dependencies-only']); + }); + + it('Should return dependencies parameter', () => { + const service = new ScanService({ + outputFilepath: '', + inputFilepath: '', + runtimeContainer: RUNTIME_CONTAINER, + dependencyScope: '', + dependencyScopeInclude: '', + dependencyScopeExclude: '', + dependenciesEnabled: true, + scanFiles: true, + skipSnippets: false, + settingsFilePath: '', + scanossSettings: false + }); + + const command = (service as any).buildDependenciesArgs(); + expect(command).toEqual(['--dependencies']); + }); + + it('Should return skip snippet parameter', () => { + const service = new ScanService({ + outputFilepath: '', + inputFilepath: '', + runtimeContainer: RUNTIME_CONTAINER, + dependencyScope: '', + dependencyScopeInclude: '', + dependencyScopeExclude: '', + dependenciesEnabled: true, + scanFiles: true, + skipSnippets: true, + settingsFilePath: '', + scanossSettings: false + }); + + const command = (service as any).buildSnippetArgs(); + expect(command).toEqual(['-S']); + }); + + it('Should return a command with skip snippet and prod dependencies', async () => { + const service = new ScanService({ + outputFilepath: 'results.json', + inputFilepath: 'inputFilepath', + runtimeContainer: RUNTIME_CONTAINER, + dependencyScope: 'prod', + dependencyScopeInclude: '', + dependencyScopeExclude: '', + dependenciesEnabled: true, + scanFiles: true, + skipSnippets: true, + settingsFilePath: '', + scanossSettings: false + }); + + const command = await (service as any).buildArgs(); + console.log(command); + expect(command).not.toBe(''); + }); + + it('Should scan dependencies', async () => { + appInput.OUTPUT_FILEPATH = 'test-results.json'; + const TEST_DIR = __dirname; + const resultPath = path.join(TEST_DIR, 'data', 'test-results.json'); + const service = new ScanService({ + outputFilepath: resultPath, + inputFilepath: path.join(TEST_DIR, 'data'), + runtimeContainer: RUNTIME_CONTAINER, + dependencyScopeInclude: '', + dependencyScopeExclude: '', + dependenciesEnabled: true, + scanFiles: true, + skipSnippets: false, + settingsFilePath: 'scanoss.json', + scanossSettings: false + }); + + const { scan } = await service.scan(); + expect(scan['package.json'][0].dependencies.length).toBeGreaterThan(0); + await fs.promises.rm(resultPath); + }, 30000); +}); diff --git a/__tests__/undeclared-argument-builder.test.ts b/__tests__/undeclared-argument-builder.test.ts new file mode 100644 index 0000000..8793fc1 --- /dev/null +++ b/__tests__/undeclared-argument-builder.test.ts @@ -0,0 +1,57 @@ +import { RUNTIME_CONTAINER } from '../src/app.input'; +import { UndeclaredArgumentBuilder } from '../src/policies/argument_builders/undeclared-argument-builder'; + +jest.mock('../src/app.input', () => ({ + ...jest.requireActual('../src/app.input'), + REPO_DIR: '', + OUTPUT_FILEPATH: 'results.json', + COPYLEFT_LICENSE_EXCLUDE: '', + COPYLEFT_LICENSE_EXPLICIT: '', + COPYLEFT_LICENSE_INCLUDE: '', + SCANOSS_SETTINGS: true, + SBOM_ENABLED: false +})); + +describe('UndeclaredArgumentBuilder', () => { + const appInput = jest.requireMock('../src/app.input'); + + it('Build Command test', async function () { + appInput.REPO_DIR = 'repodir'; + appInput.OUTPUT_FILEPATH = 'results.json'; + appInput.SCANOSS_SETTINGS = false; + const builder = new UndeclaredArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'repodir:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'undeclared', + '--input', + 'results.json', + '--format', + 'md' + ]); + }); + + it('Build Command style scanoss.json', async function () { + appInput.REPO_DIR = 'repodir'; + appInput.OUTPUT_FILEPATH = 'results.json'; + appInput.SCANOSS_SETTINGS = true; + const builder = new UndeclaredArgumentBuilder(); + const cmd = await builder.build(); + expect(cmd).toEqual([ + 'run', + '-v', + 'repodir:/scanoss', + RUNTIME_CONTAINER, + 'inspect', + 'undeclared', + '--input', + 'results.json', + '--format', + 'md' + ]); + }); +}); diff --git a/__tests__/undeclared-policy-check.test.ts b/__tests__/undeclared-policy-check.test.ts index b3d5093..6383eed 100644 --- a/__tests__/undeclared-policy-check.test.ts +++ b/__tests__/undeclared-policy-check.test.ts @@ -1,9 +1,15 @@ import { CONCLUSION, PolicyCheck } from '../src/policies/policy-check'; -import { ScannerResults } from '../src/services/result.interfaces'; -import { resultsMock } from './results.mock'; import { UndeclaredPolicyCheck } from '../src/policies/undeclared-policy-check'; -import * as sbomUtils from '../src/utils/sbom.utils'; -import { sbomMock } from './sbom.mock'; +import path from 'path'; + +jest.mock('../src/app.input', () => ({ + ...jest.requireActual('../src/app.input'), + REPO_DIR: '', + OUTPUT_FILEPATH: 'results.json', + COPYLEFT_LICENSE_EXCLUDE: '', + COPYLEFT_LICENSE_EXPLICIT: '', + COPYLEFT_LICENSE_INCLUDE: '' +})); // Mock the @actions/github module jest.mock('@actions/github', () => ({ @@ -23,8 +29,8 @@ jest.mock('@actions/github', () => ({ })); describe('UndeclaredPolicyCheck', () => { - let scannerResults: ScannerResults; let undeclaredPolicyCheck: UndeclaredPolicyCheck; + const appInput = jest.requireMock('../src/app.input'); beforeEach(() => { jest.clearAllMocks(); @@ -34,30 +40,32 @@ describe('UndeclaredPolicyCheck', () => { jest.spyOn(PolicyCheck.prototype, 'initStatus').mockImplementation(); jest.spyOn(UndeclaredPolicyCheck.prototype, 'updateCheck').mockImplementation(); - scannerResults = JSON.parse(resultsMock[3].content); - undeclaredPolicyCheck = new UndeclaredPolicyCheck(); - }); + }, 30000); it('should pass the policy check when undeclared components are not found', async () => { - jest.spyOn(sbomUtils, 'parseSBOM').mockImplementation(async () => Promise.resolve(sbomMock[1])); + const TEST_DIR = __dirname; + const TEST_REPO_DIR = path.join(TEST_DIR, 'data'); + const TEST_RESULTS_FILE = 'empty-results.json'; - await undeclaredPolicyCheck.run(scannerResults); + // Set the required environment variables + appInput.REPO_DIR = TEST_REPO_DIR; + appInput.OUTPUT_FILEPATH = TEST_RESULTS_FILE; + + await undeclaredPolicyCheck.run(); expect(undeclaredPolicyCheck.conclusion).toEqual(CONCLUSION.Success); - }); + }, 30000); it('should fail the policy check when undeclared components are found', async () => { - jest.spyOn(sbomUtils, 'parseSBOM').mockImplementation(async () => Promise.resolve(sbomMock[0])); + const TEST_DIR = __dirname; + const TEST_REPO_DIR = path.join(TEST_DIR, 'data'); + const TEST_RESULTS_FILE = 'results.json'; - await undeclaredPolicyCheck.run(scannerResults); - expect(undeclaredPolicyCheck.conclusion).toEqual(CONCLUSION.Neutral); - }); + // Set the required environment variables + appInput.REPO_DIR = TEST_REPO_DIR; + appInput.OUTPUT_FILEPATH = TEST_RESULTS_FILE; - it('should exceeded the max limit', async () => { - jest.spyOn(sbomUtils, 'parseSBOM').mockImplementation(async () => Promise.resolve(sbomMock[0])); - scannerResults = JSON.parse(resultsMock[6].content); - await undeclaredPolicyCheck.run(scannerResults); - // Neutral = Failure on test environment + await undeclaredPolicyCheck.run(); expect(undeclaredPolicyCheck.conclusion).toEqual(CONCLUSION.Neutral); - }); + }, 30000); }); diff --git a/action.yml b/action.yml index c686fe7..c4814f2 100644 --- a/action.yml +++ b/action.yml @@ -14,18 +14,6 @@ inputs: description: 'Halt if a check fails' required: false default: true - sbom.enabled: - description: 'Enable SBOM Identify' - required: false - default: true - sbom.filepath: - description: 'SBOM filepath' - required: false - default: 'sbom.json' - sbom.type: - description: 'SBOM type (identify | ignore)' - required: false - default: 'identify' api.key: description: 'SCANOSS API Key token (optional - not required for default OSSKB URL)' required: false @@ -66,7 +54,23 @@ inputs: required: false runtimeContainer: description: 'Specify runtime container to perform the scan.' - default: 'ghcr.io/scanoss/scanoss-py:v1.15.0' + default: 'ghcr.io/scanoss/scanoss-py:v1.19.0' + required: false + skipSnippets: + description: 'Skip the generation of snippets.' + default: false + required: false + scanFiles: + description: 'Enable or disable file and snippet scanning.' + default: true + required: false + scanossSettings: + description: 'Settings file to use for scanning.' + default: true + required: false + settingsFilepath: + description: 'SCANOSS settings file path.' + default: 'scanoss.json' required: false diff --git a/dist/index.js b/dist/index.js index eaccdff..0877612 100644 --- a/dist/index.js +++ b/dist/index.js @@ -125780,13 +125780,10 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.RUNTIME_CONTAINER = exports.REPO_DIR = exports.COPYLEFT_LICENSE_EXPLICIT = exports.COPYLEFT_LICENSE_EXCLUDE = exports.COPYLEFT_LICENSE_INCLUDE = exports.GITHUB_TOKEN = exports.OUTPUT_FILEPATH = exports.API_URL = exports.API_KEY = exports.DEPENDENCY_SCOPE_INCLUDE = exports.DEPENDENCY_SCOPE_EXCLUDE = exports.DEPENDENCIES_SCOPE = exports.DEPENDENCIES_ENABLED = exports.SBOM_TYPE = exports.SBOM_FILEPATH = exports.SBOM_ENABLED = exports.POLICIES_HALT_ON_FAILURE = exports.POLICIES = void 0; +exports.EXECUTABLE = exports.SETTINGS_FILE_PATH = exports.SCANOSS_SETTINGS = exports.SCAN_FILES = exports.SKIP_SNIPPETS = exports.RUNTIME_CONTAINER = exports.REPO_DIR = exports.COPYLEFT_LICENSE_EXPLICIT = exports.COPYLEFT_LICENSE_EXCLUDE = exports.COPYLEFT_LICENSE_INCLUDE = exports.GITHUB_TOKEN = exports.OUTPUT_FILEPATH = exports.API_URL = exports.API_KEY = exports.DEPENDENCY_SCOPE_INCLUDE = exports.DEPENDENCY_SCOPE_EXCLUDE = exports.DEPENDENCIES_SCOPE = exports.DEPENDENCIES_ENABLED = exports.POLICIES_HALT_ON_FAILURE = exports.POLICIES = void 0; const core = __importStar(__nccwpck_require__(42186)); exports.POLICIES = core.getInput('policies'); exports.POLICIES_HALT_ON_FAILURE = core.getInput('policies.halt_on_failure') === 'true'; -exports.SBOM_ENABLED = core.getInput('sbom.enabled') === 'true'; -exports.SBOM_FILEPATH = core.getInput('sbom.filepath'); -exports.SBOM_TYPE = core.getInput('sbom.type'); exports.DEPENDENCIES_ENABLED = core.getInput('dependencies.enabled') === 'true'; exports.DEPENDENCIES_SCOPE = core.getInput('dependencies.scope'); exports.DEPENDENCY_SCOPE_EXCLUDE = core.getInput('dependencies.scope.exclude'); @@ -125799,7 +125796,12 @@ exports.COPYLEFT_LICENSE_INCLUDE = core.getInput('licenses.copyleft.include'); exports.COPYLEFT_LICENSE_EXCLUDE = core.getInput('licenses.copyleft.exclude'); exports.COPYLEFT_LICENSE_EXPLICIT = core.getInput('licenses.copyleft.explicit'); exports.REPO_DIR = process.env.GITHUB_WORKSPACE; -exports.RUNTIME_CONTAINER = core.getInput('runtimeContainer') || 'ghcr.io/scanoss/scanoss-py:v1.15.0'; +exports.RUNTIME_CONTAINER = core.getInput('runtimeContainer') || 'ghcr.io/scanoss/scanoss-py:v1.19.0'; +exports.SKIP_SNIPPETS = core.getInput('skipSnippets') === 'true'; +exports.SCAN_FILES = core.getInput('scanFiles') === 'true'; +exports.SCANOSS_SETTINGS = core.getInput('scanossSettings') === 'true'; +exports.SETTINGS_FILE_PATH = core.getInput('settingsFilepath') || 'scanoss.json'; +exports.EXECUTABLE = 'docker'; /***/ }), @@ -125918,7 +125920,7 @@ async function run() { await (0, scan_service_1.uploadResults)(); // run policies for (const policy of policies) { - await policy.run(scan); + await policy.run(); } if ((0, github_utils_1.isPullRequest)()) { // create reports @@ -125941,7 +125943,135 @@ exports.run = run; /***/ }), -/***/ 34466: +/***/ 26658: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.ArgumentBuilder = void 0; +class ArgumentBuilder { +} +exports.ArgumentBuilder = ArgumentBuilder; + + +/***/ }), + +/***/ 84156: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2024, 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. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CopyLeftArgumentBuilder = void 0; +const argument_builder_1 = __nccwpck_require__(26658); +const app_input_1 = __nccwpck_require__(483); +const core = __importStar(__nccwpck_require__(42186)); +class CopyLeftArgumentBuilder extends argument_builder_1.ArgumentBuilder { + buildCopyleftArgs() { + if (app_input_1.COPYLEFT_LICENSE_EXPLICIT) { + core.info(`Explicit copyleft licenses: ${app_input_1.COPYLEFT_LICENSE_EXPLICIT}`); + return ['--explicit', app_input_1.COPYLEFT_LICENSE_EXPLICIT]; + } + if (app_input_1.COPYLEFT_LICENSE_INCLUDE) { + core.info(`Included copyleft licenses: ${app_input_1.COPYLEFT_LICENSE_INCLUDE}`); + return ['--include', app_input_1.COPYLEFT_LICENSE_INCLUDE]; + } + if (app_input_1.COPYLEFT_LICENSE_EXCLUDE) { + core.info(`Excluded copyleft licenses: ${app_input_1.COPYLEFT_LICENSE_EXCLUDE}`); + return ['--exclude', app_input_1.COPYLEFT_LICENSE_EXCLUDE]; + } + return []; + } + async build() { + return [ + 'run', + '-v', + `${app_input_1.REPO_DIR}:/scanoss`, + app_input_1.RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + app_input_1.OUTPUT_FILEPATH, + '--format', + 'md', + ...this.buildCopyleftArgs() + ]; + } +} +exports.CopyLeftArgumentBuilder = CopyLeftArgumentBuilder; + + +/***/ }), + +/***/ 48628: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -125969,12 +126099,88 @@ exports.run = run; THE SOFTWARE. */ Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.UndeclaredArgumentBuilder = void 0; +const argument_builder_1 = __nccwpck_require__(26658); +const app_input_1 = __nccwpck_require__(483); +class UndeclaredArgumentBuilder extends argument_builder_1.ArgumentBuilder { + async build() { + return [ + 'run', + '-v', + `${app_input_1.REPO_DIR}:/scanoss`, + app_input_1.RUNTIME_CONTAINER, + 'inspect', + 'undeclared', + '--input', + app_input_1.OUTPUT_FILEPATH, + '--format', + 'md' + ]; + } +} +exports.UndeclaredArgumentBuilder = UndeclaredArgumentBuilder; + + +/***/ }), + +/***/ 34466: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2024, 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. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); exports.CopyleftPolicyCheck = void 0; +const core = __importStar(__nccwpck_require__(42186)); const app_config_1 = __nccwpck_require__(29014); const policy_check_1 = __nccwpck_require__(63702); -const result_service_1 = __nccwpck_require__(32414); -const markdown_utils_1 = __nccwpck_require__(96011); -const license_utils_1 = __nccwpck_require__(52210); +const app_input_1 = __nccwpck_require__(483); +const exec = __importStar(__nccwpck_require__(71514)); +const copyleft_argument_builder_1 = __nccwpck_require__(84156); /** * This class checks if any of the components identified in the scanner results are subject to copyleft licenses. * It filters components based on their licenses and looks for those with copyleft obligations. @@ -125982,77 +126188,32 @@ const license_utils_1 = __nccwpck_require__(52210); */ class CopyleftPolicyCheck extends policy_check_1.PolicyCheck { static policyName = 'Copyleft Policy'; - copyleftLicenses = new Set([ - 'GPL-1.0-only', - 'GPL-2.0-only', - 'GPL-3.0-only', - 'AGPL-3.0-only', - 'Sleepycat', - 'Watcom-1.0', - 'GFDL-1.1-only', - 'GFDL-1.2-only', - 'GFDL-1.3-only', - 'LGPL-2.1-only', - 'LGPL-3.0-only', - 'MPL-1.1', - 'MPL-2.0', - 'EPL-1.0', - 'EPL-2.0', - 'CDDL-1.0', - 'CDDL-1.1', - 'CECILL-2.1', - 'Artistic-1.0', - 'Artistic-2.0', - 'CC-BY-SA-4.0' - ].map(l => l.toLowerCase())); - constructor() { + argumentBuilder; + constructor(argumentBuilder = new copyleft_argument_builder_1.CopyLeftArgumentBuilder()) { super(`${app_config_1.CHECK_NAME}: ${CopyleftPolicyCheck.policyName}`); + this.argumentBuilder = argumentBuilder; } - async run(scannerResults) { + async run() { + core.info(`Running Copyleft Policy Check...`); super.initStatus(); - const components = (0, result_service_1.getComponents)(scannerResults); - // Filter copyleft components - const componentsWithCopyleft = components.filter(component => component.licenses.some(license => !!license.copyleft || license_utils_1.licenseUtil.isCopyLeft(license.spdxid.trim().toLowerCase()))); - const summary = this.getSummary(componentsWithCopyleft); - let details = this.getDetails(componentsWithCopyleft); - if (details) { - const { id } = await this.uploadArtifact(details); - if (id) - details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); - } - if (componentsWithCopyleft.length === 0) { - return this.success(summary, details); + const args = await this.argumentBuilder.build(); + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + const { stdout, stderr, exitCode } = await exec.getExecOutput(app_input_1.EXECUTABLE, args, options); + const summary = stdout; + let details = stderr; + if (exitCode === 1) { + await this.success('### :white_check_mark: Policy Pass \n #### Not copyleft Licenses were found', undefined); + return; } - else { - return this.reject(summary, details); + const { id } = await this.uploadArtifact(stdout); + core.debug(`Copyleft Artifact ID: ${id}`); + if (id) { + details = await this.concatPolicyArtifactURLToPolicyCheck(stderr, id); } - } - getSummary(components) { - return components.length === 0 - ? '### :white_check_mark: Policy Pass \n #### Not copyleft components were found' - : `### :x: Policy Fail \n #### ${components.length} component(s) with copyleft licenses were found. \n See details for more information.`; - } - getDetails(components) { - if (components.length === 0) - return undefined; - const headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']; - const centeredColumns = [1, 4]; - const rows = []; - components.forEach(component => { - component.licenses.forEach(license => { - if (license_utils_1.licenseUtil.isCopyLeft(license.spdxid?.trim().toLowerCase())) { - const copyleftIcon = license_utils_1.licenseUtil.isCopyLeft(license.spdxid?.trim().toLowerCase()) ? 'YES' : 'NO'; - rows.push([ - component.purl, - component.version, - license.spdxid, - `${license_utils_1.licenseUtil.getOSADL(license?.spdxid) || ''}`, - copyleftIcon - ]); - } - }); - }); - return `### Copyleft licenses \n ${(0, markdown_utils_1.generateTable)(headers, rows, centeredColumns)}`; + return this.reject(summary, details); } artifactPolicyFileName() { return 'policy-check-copyleft-results.md'; @@ -126388,11 +126549,10 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.UndeclaredPolicyCheck = void 0; const policy_check_1 = __nccwpck_require__(63702); const app_config_1 = __nccwpck_require__(29014); -const result_service_1 = __nccwpck_require__(32414); -const inputs = __importStar(__nccwpck_require__(483)); const core = __importStar(__nccwpck_require__(42186)); -const sbom_utils_1 = __nccwpck_require__(31156); -const markdown_utils_1 = __nccwpck_require__(96011); +const app_input_1 = __nccwpck_require__(483); +const exec = __importStar(__nccwpck_require__(71514)); +const undeclared_argument_builder_1 = __nccwpck_require__(48628); /** * Verifies that all components identified in scanner results are declared in the project's SBOM. * The run method compares components found by the scanner against those declared in the SBOM. @@ -126402,59 +126562,35 @@ const markdown_utils_1 = __nccwpck_require__(96011); */ class UndeclaredPolicyCheck extends policy_check_1.PolicyCheck { static policyName = 'Undeclared Policy'; - constructor() { + argumentBuilder; + constructor(argumentBuilder = new undeclared_argument_builder_1.UndeclaredArgumentBuilder()) { super(`${app_config_1.CHECK_NAME}: ${UndeclaredPolicyCheck.policyName}`); + this.argumentBuilder = argumentBuilder; } - async run(scannerResults) { + async run() { + core.info(`Running Undeclared Components Policy Check...`); super.initStatus(); - const nonDeclaredComponents = []; - let declaredComponents = []; - const comps = (0, result_service_1.getComponents)(scannerResults); - // get declared components - try { - const sbom = await (0, sbom_utils_1.parseSBOM)(inputs.SBOM_FILEPATH); - declaredComponents = sbom.components || []; - } - catch (e) { - core.info(`Warning on policy check: ${this.checkName}. SBOM file could not be parsed (${inputs.SBOM_FILEPATH})`); - } - comps.forEach(c => { - if (!declaredComponents.some(component => component.purl === c.purl)) { - nonDeclaredComponents.push(c); - } - }); - const summary = this.getSummary(nonDeclaredComponents); - let details = this.getDetails(nonDeclaredComponents); - if (details) { - const { id } = await this.uploadArtifact(details); - if (id) - details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); - } - if (nonDeclaredComponents.length === 0) { - return this.success(summary, details); - } - else { - return this.reject(summary, details); + const args = await this.argumentBuilder.build(); + core.debug(`Args: ${args}`); + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + const { stdout, stderr, exitCode } = await exec.getExecOutput(app_input_1.EXECUTABLE, args, options); + const summary = stdout; + let details = stderr; + if (!app_input_1.SCANOSS_SETTINGS) { + core.warning('Undeclared policy is being used with SCANOSS settings disabled'); + } + if (exitCode === 1) { + await this.success('### :white_check_mark: Policy Pass \n #### Not undeclared components were found', undefined); + return; } - } - getSummary(components) { - return components.length === 0 - ? '### :white_check_mark: Policy Pass \n #### Not undeclared components were found' - : `### :x: Policy Fail \n #### ${components.length} undeclared component(s) were found. \n See details for more information.`; - } - getDetails(components) { - if (components.length === 0) - return undefined; - const headers = ['Component', 'Version', 'License']; - const rows = []; - components.forEach(component => { - const licenses = component.licenses.map(l => l.spdxid).join(' - '); - rows.push([component.purl, component.version, licenses]); - }); - const snippet = JSON.stringify(components.map(({ purl }) => ({ purl })), null, 4); - let content = `### Undeclared components \n ${(0, markdown_utils_1.generateTable)(headers, rows)}`; - content += `#### Add the following snippet into your \`sbom.json\` file \n \`\`\`json \n ${snippet} \n \`\`\``; - return content; + const { id } = await this.uploadArtifact(details); + core.debug(`Undeclared Artifact ID: ${id}`); + if (id) + details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); + return this.reject(summary, details); } artifactPolicyFileName() { return 'policy-check-undeclared-results.md'; @@ -126864,22 +127000,48 @@ const inputs = __importStar(__nccwpck_require__(483)); const fs_1 = __importDefault(__nccwpck_require__(57147)); const core = __importStar(__nccwpck_require__(42186)); const path = __importStar(__nccwpck_require__(71017)); +const app_input_1 = __nccwpck_require__(483); const artifact = new artifact_1.DefaultArtifactClient(); async function uploadResults() { await artifact.uploadArtifact(path.basename(inputs.OUTPUT_FILEPATH), [inputs.OUTPUT_FILEPATH], path.dirname(inputs.OUTPUT_FILEPATH)); } exports.uploadResults = uploadResults; /** - * `ScanService` is a class that wraps the `scanoss.py` Docker image, providing a simplified interface - * for configuring and executing source code scans + * @class ScannerService + * @brief A service class that manages scanning operations using the scanoss-py Docker container + * + * @details + * The ScannerService class provides functionality to scan repositories for: + * - File scanning + * - Dependency analysis + * + * @property {Options} options - Configuration options for the scanner + * @property {string} options.apiKey - API key for SCANOSS service authentication + * @property {string} options.apiUrl - URL endpoint for the SCANOSS service + * @property {boolean} options.sbomEnabled - Flag to enable SBOM generation + * @property {string} options.sbomFilepath - Path to store or read SBOM files + * @property {string} options.sbomType - Type of SBOM format to use + * @property {boolean} options.dependenciesEnabled - Flag to enable dependency scanning + * @property {string} options.outputFilepath - Path for scan results output + * @property {string} options.inputFilepath - Path to the repository to scan + * @property {string} options.runtimeContainer - Docker container image to use + * @property {string} options.dependencyScope - Scope for dependency scanning (prod/dev) + * @property {string} options.dependencyScopeExclude - Dependencies to exclude from scan + * @property {string} options.dependencyScopeInclude - Dependencies to include in scan + * @property {boolean} options.skipSnippets - Flag to skip snippet scanning + * @property {boolean} options.scanFiles - Flag to enable file scanning + * @property {boolean} options.scanossSettings - Flag to enable SCANOSS Settings + * @property {boolean} options.settingsFilePath - Path to settings file + * + * @throws {Error} When required configuration options are missing or invalid + * + * @author [SCANOSS] */ class ScanService { options; + DEFAULT_SETTING_FILE_PATH = 'scanoss.json'; constructor(options) { this.options = options || { - sbomFilepath: inputs.SBOM_FILEPATH, - sbomType: inputs.SBOM_TYPE, - sbomEnabled: inputs.SBOM_ENABLED, apiKey: inputs.API_KEY, apiUrl: inputs.API_URL, dependenciesEnabled: inputs.DEPENDENCIES_ENABLED, @@ -126888,40 +127050,151 @@ class ScanService { dependencyScope: inputs.DEPENDENCIES_SCOPE, dependencyScopeInclude: inputs.DEPENDENCY_SCOPE_INCLUDE, dependencyScopeExclude: inputs.DEPENDENCY_SCOPE_EXCLUDE, - runtimeContainer: inputs.RUNTIME_CONTAINER + runtimeContainer: inputs.RUNTIME_CONTAINER, + skipSnippets: app_input_1.SKIP_SNIPPETS, + scanFiles: app_input_1.SCAN_FILES, + scanossSettings: app_input_1.SCANOSS_SETTINGS, + settingsFilePath: app_input_1.SETTINGS_FILE_PATH }; } + /** + * @brief Executes the scanning process using a scanoss-py Docker container + * @throws {Error} When Docker command fails or configuration is invalid + * @returns {Promise} The results of the scanning operation + * + * @details + * This method performs the following operations: + * - Validates basic configuration + * - Executes Docker command + * - Uploads results to artifacts + * - Parses and returns results + * + * @note At least one scan option (scanFiles or dependenciesEnabled) must be enabled + */ async scan() { - const command = await this.buildCommand(); - const { stdout, stderr } = await exec.getExecOutput(command, []); + // Check for basic configuration before running the docker container + this.checkBasicConfig(); + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + const args = await this.buildArgs(); + const { stdout, stderr } = await exec.getExecOutput(app_input_1.EXECUTABLE, args, options); const scan = await this.parseResult(); return { scan, stdout, stderr }; } - dependencyScopeCommand() { + /** + * @brief Builds the dependency scope command string + * @returns {Array} The formatted dependency scope command + * + * @details + * Handles three possible scope configurations: + * - Dependency scope exclude + * - Dependency scope include + * - Dependency scope (prod/dev) + * + * @throws {Error} When multiple dependency scope filters are set + * + * @note Only one dependency scope filter can be set at a time + */ + dependencyScopeArgs() { const { dependencyScopeInclude, dependencyScopeExclude, dependencyScope } = this.options; // Count the number of non-empty values const setScopes = [dependencyScopeInclude, dependencyScopeExclude, dependencyScope].filter(scope => scope !== '' && scope !== undefined); if (setScopes.length > 1) { - core.setFailed('Only one dependency scope filter can be set'); - } - if (dependencyScopeExclude !== '') - return `--dep-scope-exc ${this.options.dependencyScopeExclude}`; - if (dependencyScopeInclude !== '') - return `--dep-scope-inc ${this.options.dependencyScopeInclude}`; - if (dependencyScope === 'prod') - return '--dep-scope prod'; - if (dependencyScope === 'dev') - return '--dep-scope dev'; - return ''; + core.error('Only one dependency scope filter can be set'); + } + if (dependencyScopeExclude && dependencyScopeExclude !== '') + return ['--dep-scope-exc', dependencyScopeExclude]; + if (dependencyScopeInclude && dependencyScopeInclude !== '') + return ['--dep-scope-inc', dependencyScopeInclude]; + if (dependencyScope && dependencyScope === 'prod') + return ['--dep-scope', 'prod']; + if (dependencyScope && dependencyScope === 'dev') + return ['--dep-scope', 'dev']; + return []; } - async buildCommand() { - return `docker run -v "${this.options.inputFilepath}":"/scanoss" ${this.options.runtimeContainer} scan . - --output ${this.options.outputFilepath} - ${this.options.dependenciesEnabled ? `--dependencies` : ''} - ${this.dependencyScopeCommand()} - ${await this.detectSBOM()} - ${this.options.apiUrl ? `--apiurl ${this.options.apiUrl}` : ''} - ${this.options.apiKey ? `--key ${this.options.apiKey}` : ''}`.replace(/\n/gm, ' '); + /** + * @brief Generates the snippet-related portion of the Docker command + * @returns {Array} The snippet command flag (-S) or empty string + * + * @details + * Returns ["-S"] if snippets should be skipped, empty string otherwise + */ + buildSnippetArgs() { + if (!this.options.skipSnippets) + return []; + return ['-S']; + } + /** + * @brief Constructs the dependencies cmd + * @returns {Array} The formatted dependencies command + * + * @details + * Combines dependency scanning options with scope commands. + * Possible return values: + * - [--dependencies-only, ${scopeCmd}]' + * - [--dependencies, ${scopeCmd}] + * - Empty array if no dependencies scanning is needed + */ + buildDependenciesArgs() { + const dependencyScopeCmd = this.dependencyScopeArgs(); + if (!this.options.scanFiles && this.options.dependenciesEnabled) { + return ['--dependencies-only', ...dependencyScopeCmd]; + } + else if (this.options.dependenciesEnabled) { + return ['--dependencies', ...dependencyScopeCmd]; + } + return []; + } + /** + * @brief Assembles the complete Docker command string + * @returns {Promise>} The complete Docker command + * + * @details + * Combines all command components: + * - Docker run command with volume mounting + * - Runtime container specification + * - Scan command with output file + * - Dependencies command + * - SBOM detection + * - Snippet command + * - API configuration + * + */ + async buildArgs() { + return [ + 'run', + '-v', + `${this.options.inputFilepath}:/scanoss`, + this.options.runtimeContainer, + 'scan', + '.', + '--output', + `./${app_input_1.OUTPUT_FILEPATH}`, + ...this.buildDependenciesArgs(), + ...(await this.detectSBOM()), + ...this.buildSnippetArgs(), + ...(this.options.apiUrl ? ['--apiurl', this.options.apiUrl] : []), + ...(this.options.apiKey ? ['--apiKey', this.options.apiKey.replace(/\n/gm, ' ')] : []) + ]; + } + /** + * @brief Validates the basic configuration requirements for scanning + * + * @throws {Error} When no scan options are enabled + * + * @details + * This method ensures that at least one of the following scan options is enabled: + * - scanFiles: For scanning source code files + * - dependenciesEnabled: For scanning project dependencies + * + */ + checkBasicConfig() { + if (!this.options.scanFiles && !this.options.dependenciesEnabled) { + core.error(`At least one scan option should be enabled: [scanFiles, dependencyEnabled]`); + } + core.info('Basic scan config is valid'); } /** * Constructs the command segment for SBOM ingestion based on the current configuration. This method checks if SBOM @@ -126936,16 +127209,22 @@ class ScanService { * @private */ async detectSBOM() { - if (!this.options.sbomEnabled || !this.options.sbomFilepath) - return ''; - try { - await fs_1.default.promises.access(this.options.sbomFilepath, fs_1.default.constants.F_OK); - return `--${this.options.sbomType} ${this.options.sbomFilepath}`; - } - catch (error) { - core.warning('SBOM not found'); - return ''; + // Overrides sbom file if is set + if (this.options.scanossSettings) { + try { + await fs_1.default.promises.access(this.options.settingsFilePath, fs_1.default.constants.F_OK); + return ['--settings', this.options.settingsFilePath]; + } + catch (error) { + if (this.options.settingsFilePath === this.DEFAULT_SETTING_FILE_PATH) + return []; + core.warning(`SCANOSS settings file not found at '${this.options.settingsFilePath}'. + Please provide a valid SCANOSS settings file path.`); + return []; + } } + // Force scanoss.py to not load the settings.json file + return ['-stf']; } async parseResult() { const content = await fs_1.default.promises.readFile(this.options.outputFilepath, 'utf-8'); @@ -127224,47 +127503,6 @@ const generateTable = (headers, rows, centeredColumns) => { exports.generateTable = generateTable; -/***/ }), - -/***/ 31156: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -// SPDX-License-Identifier: MIT -/* - Copyright (c) 2024, 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. - */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.parseSBOM = void 0; -const fs_1 = __importDefault(__nccwpck_require__(57147)); -async function parseSBOM(filepath) { - return JSON.parse(await fs_1.default.promises.readFile(filepath, 'utf-8')); -} -exports.parseSBOM = parseSBOM; - - /***/ }), /***/ 22877: diff --git a/package-lock.json b/package-lock.json index 3e579d8..908c7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "scanoss-code-scan-action", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scanoss-code-scan-action", - "version": "0.2.2", + "version": "0.2.3", "license": "MIT", "dependencies": { "@actions/artifact": "^2.1.0", diff --git a/package.json b/package.json index ba167eb..33652a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "scanoss-code-scan-action", "description": "SCANOSS Code Scan Action", - "version": "0.2.3", + "version": "1.0.1", "author": "SCANOSS", "private": true, "homepage": "https://github.com/scanoss/code-scan-action/", diff --git a/policy-check-undeclared-results.md b/policy-check-undeclared-results.md new file mode 100644 index 0000000..6d70fcd --- /dev/null +++ b/policy-check-undeclared-results.md @@ -0,0 +1,25 @@ +5 undeclared component(s) were found. +Add the following snippet into your `sbom.json` file + +```json +{ + "components": [ + { + "purl": "pkg:github/scanoss/wfp" + }, + { + "purl": "pkg:github/scanoss/scanner.c" + }, + { + "purl": "pkg:npm/%40grpc/grpc-js" + }, + { + "purl": "pkg:npm/abort-controller" + }, + { + "purl": "pkg:npm/adm-zip" + } + ] +} +``` + diff --git a/src/app.input.ts b/src/app.input.ts index abce57d..f6742a6 100644 --- a/src/app.input.ts +++ b/src/app.input.ts @@ -25,9 +25,6 @@ import * as core from '@actions/core'; export const POLICIES = core.getInput('policies'); export const POLICIES_HALT_ON_FAILURE = core.getInput('policies.halt_on_failure') === 'true'; -export const SBOM_ENABLED = core.getInput('sbom.enabled') === 'true'; -export const SBOM_FILEPATH = core.getInput('sbom.filepath'); -export const SBOM_TYPE = core.getInput('sbom.type'); export const DEPENDENCIES_ENABLED = core.getInput('dependencies.enabled') === 'true'; export const DEPENDENCIES_SCOPE = core.getInput('dependencies.scope'); export const DEPENDENCY_SCOPE_EXCLUDE = core.getInput('dependencies.scope.exclude'); @@ -40,4 +37,9 @@ export const COPYLEFT_LICENSE_INCLUDE = core.getInput('licenses.copyleft.include export const COPYLEFT_LICENSE_EXCLUDE = core.getInput('licenses.copyleft.exclude'); export const COPYLEFT_LICENSE_EXPLICIT = core.getInput('licenses.copyleft.explicit'); export const REPO_DIR = process.env.GITHUB_WORKSPACE as string; -export const RUNTIME_CONTAINER = core.getInput('runtimeContainer') || 'ghcr.io/scanoss/scanoss-py:v1.15.0'; +export const RUNTIME_CONTAINER = core.getInput('runtimeContainer') || 'ghcr.io/scanoss/scanoss-py:v1.19.0'; +export const SKIP_SNIPPETS = core.getInput('skipSnippets') === 'true'; +export const SCAN_FILES = core.getInput('scanFiles') === 'true'; +export const SCANOSS_SETTINGS = core.getInput('scanossSettings') === 'true'; +export const SETTINGS_FILE_PATH = core.getInput('settingsFilepath') || 'scanoss.json'; +export const EXECUTABLE = 'docker'; diff --git a/src/main.ts b/src/main.ts index 49c0a99..4748889 100644 --- a/src/main.ts +++ b/src/main.ts @@ -54,7 +54,7 @@ export async function run(): Promise { // run policies for (const policy of policies) { - await policy.run(scan); + await policy.run(); } if (isPullRequest()) { diff --git a/src/utils/sbom.utils.ts b/src/policies/argument_builders/argument-builder.ts similarity index 83% rename from src/utils/sbom.utils.ts rename to src/policies/argument_builders/argument-builder.ts index 124b079..9c19292 100644 --- a/src/utils/sbom.utils.ts +++ b/src/policies/argument_builders/argument-builder.ts @@ -21,14 +21,6 @@ THE SOFTWARE. */ -import fs from 'fs'; - -export interface SBOM { - components: { - purl: string; - }[]; -} - -export async function parseSBOM(filepath: string): Promise { - return JSON.parse(await fs.promises.readFile(filepath, 'utf-8')) as SBOM; +export abstract class ArgumentBuilder { + abstract build(): Promise; } diff --git a/src/policies/argument_builders/copyleft-argument-builder.ts b/src/policies/argument_builders/copyleft-argument-builder.ts new file mode 100644 index 0000000..944e7ee --- /dev/null +++ b/src/policies/argument_builders/copyleft-argument-builder.ts @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2024, 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 { ArgumentBuilder } from './argument-builder'; +import { + COPYLEFT_LICENSE_EXCLUDE, + COPYLEFT_LICENSE_EXPLICIT, + COPYLEFT_LICENSE_INCLUDE, + OUTPUT_FILEPATH, + REPO_DIR, + RUNTIME_CONTAINER +} from '../../app.input'; +import * as core from '@actions/core'; + +export class CopyLeftArgumentBuilder extends ArgumentBuilder { + private buildCopyleftArgs(): string[] { + if (COPYLEFT_LICENSE_EXPLICIT) { + core.info(`Explicit copyleft licenses: ${COPYLEFT_LICENSE_EXPLICIT}`); + return ['--explicit', COPYLEFT_LICENSE_EXPLICIT]; + } + + if (COPYLEFT_LICENSE_INCLUDE) { + core.info(`Included copyleft licenses: ${COPYLEFT_LICENSE_INCLUDE}`); + return ['--include', COPYLEFT_LICENSE_INCLUDE]; + } + + if (COPYLEFT_LICENSE_EXCLUDE) { + core.info(`Excluded copyleft licenses: ${COPYLEFT_LICENSE_EXCLUDE}`); + return ['--exclude', COPYLEFT_LICENSE_EXCLUDE]; + } + + return []; + } + + async build(): Promise { + return [ + 'run', + '-v', + `${REPO_DIR}:/scanoss`, + RUNTIME_CONTAINER, + 'inspect', + 'copyleft', + '--input', + OUTPUT_FILEPATH, + '--format', + 'md', + ...this.buildCopyleftArgs() + ]; + } +} diff --git a/src/policies/argument_builders/undeclared-argument-builder.ts b/src/policies/argument_builders/undeclared-argument-builder.ts new file mode 100644 index 0000000..7febe19 --- /dev/null +++ b/src/policies/argument_builders/undeclared-argument-builder.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +/* + Copyright (c) 2024, 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 { ArgumentBuilder } from './argument-builder'; +import { OUTPUT_FILEPATH, REPO_DIR, RUNTIME_CONTAINER } from '../../app.input'; + +export class UndeclaredArgumentBuilder extends ArgumentBuilder { + async build(): Promise { + return [ + 'run', + '-v', + `${REPO_DIR}:/scanoss`, + RUNTIME_CONTAINER, + 'inspect', + 'undeclared', + '--input', + OUTPUT_FILEPATH, + '--format', + 'md' + ]; + } +} diff --git a/src/policies/copyleft-policy-check.ts b/src/policies/copyleft-policy-check.ts index b808ad5..c431be9 100644 --- a/src/policies/copyleft-policy-check.ts +++ b/src/policies/copyleft-policy-check.ts @@ -21,12 +21,13 @@ THE SOFTWARE. */ -import { ScannerResults } from '../services/result.interfaces'; +import * as core from '@actions/core'; import { CHECK_NAME } from '../app.config'; import { PolicyCheck } from './policy-check'; -import { Component, getComponents } from '../services/result.service'; -import { generateTable } from '../utils/markdown.utils'; -import { licenseUtil } from '../utils/license.utils'; +import { EXECUTABLE } from '../app.input'; +import * as exec from '@actions/exec'; +import { CopyLeftArgumentBuilder } from './argument_builders/copyleft-argument-builder'; +import { ArgumentBuilder } from './argument_builders/argument-builder'; /** * This class checks if any of the components identified in the scanner results are subject to copyleft licenses. @@ -35,90 +36,37 @@ import { licenseUtil } from '../utils/license.utils'; */ export class CopyleftPolicyCheck extends PolicyCheck { static policyName = 'Copyleft Policy'; - private copyleftLicenses = new Set( - [ - 'GPL-1.0-only', - 'GPL-2.0-only', - 'GPL-3.0-only', - 'AGPL-3.0-only', - 'Sleepycat', - 'Watcom-1.0', - 'GFDL-1.1-only', - 'GFDL-1.2-only', - 'GFDL-1.3-only', - 'LGPL-2.1-only', - 'LGPL-3.0-only', - 'MPL-1.1', - 'MPL-2.0', - 'EPL-1.0', - 'EPL-2.0', - 'CDDL-1.0', - 'CDDL-1.1', - 'CECILL-2.1', - 'Artistic-1.0', - 'Artistic-2.0', - 'CC-BY-SA-4.0' - ].map(l => l.toLowerCase()) - ); + private argumentBuilder: ArgumentBuilder; - constructor() { + constructor(argumentBuilder: CopyLeftArgumentBuilder = new CopyLeftArgumentBuilder()) { super(`${CHECK_NAME}: ${CopyleftPolicyCheck.policyName}`); + this.argumentBuilder = argumentBuilder; } - async run(scannerResults: ScannerResults): Promise { + async run(): Promise { + core.info(`Running Copyleft Policy Check...`); super.initStatus(); - const components = getComponents(scannerResults); - - // Filter copyleft components - const componentsWithCopyleft = components.filter(component => - component.licenses.some( - license => !!license.copyleft || licenseUtil.isCopyLeft(license.spdxid.trim().toLowerCase()) - ) - ); - - const summary = this.getSummary(componentsWithCopyleft); - let details = this.getDetails(componentsWithCopyleft); - - if (details) { - const { id } = await this.uploadArtifact(details); - if (id) details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); + const args = await this.argumentBuilder.build(); + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + + const { stdout, stderr, exitCode } = await exec.getExecOutput(EXECUTABLE, args, options); + const summary = stdout; + let details = stderr; + if (exitCode === 1) { + await this.success('### :white_check_mark: Policy Pass \n #### Not copyleft Licenses were found', undefined); + return; } - if (componentsWithCopyleft.length === 0) { - return this.success(summary, details); - } else { - return this.reject(summary, details); + const { id } = await this.uploadArtifact(stdout); + core.debug(`Copyleft Artifact ID: ${id}`); + if (id) { + details = await this.concatPolicyArtifactURLToPolicyCheck(stderr, id); } - } - - private getSummary(components: Component[]): string { - return components.length === 0 - ? '### :white_check_mark: Policy Pass \n #### Not copyleft components were found' - : `### :x: Policy Fail \n #### ${components.length} component(s) with copyleft licenses were found. \n See details for more information.`; - } - - private getDetails(components: Component[]): string | undefined { - if (components.length === 0) return undefined; - - const headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']; - const centeredColumns = [1, 4]; - const rows: string[][] = []; - components.forEach(component => { - component.licenses.forEach(license => { - if (licenseUtil.isCopyLeft(license.spdxid?.trim().toLowerCase())) { - const copyleftIcon = licenseUtil.isCopyLeft(license.spdxid?.trim().toLowerCase()) ? 'YES' : 'NO'; - rows.push([ - component.purl, - component.version, - license.spdxid, - `${licenseUtil.getOSADL(license?.spdxid) || ''}`, - copyleftIcon - ]); - } - }); - }); - return `### Copyleft licenses \n ${generateTable(headers, rows, centeredColumns)}`; + return this.reject(summary, details); } artifactPolicyFileName(): string { diff --git a/src/policies/policy-check.ts b/src/policies/policy-check.ts index 855b97b..d52d446 100644 --- a/src/policies/policy-check.ts +++ b/src/policies/policy-check.ts @@ -25,7 +25,6 @@ import { context, getOctokit } from '@actions/github'; import { promises as fs } from 'fs'; import * as core from '@actions/core'; import { getSHA } from '../utils/github.utils'; -import { ScannerResults } from '../services/result.interfaces'; import { GitHub } from '@actions/github/lib/utils'; import * as inputs from '../app.input'; import { DefaultArtifactClient, UploadArtifactResponse } from '@actions/artifact'; @@ -82,7 +81,7 @@ export abstract class PolicyCheck { abstract getPolicyName(): string; - abstract run(scannerResults: ScannerResults): Promise; + abstract run(): Promise; async start(runId: number): Promise { const result = await this.octokit.rest.checks.create({ diff --git a/src/policies/undeclared-policy-check.ts b/src/policies/undeclared-policy-check.ts index 37aa356..d1273ee 100644 --- a/src/policies/undeclared-policy-check.ts +++ b/src/policies/undeclared-policy-check.ts @@ -23,12 +23,11 @@ import { PolicyCheck } from './policy-check'; import { CHECK_NAME } from '../app.config'; -import { ScannerResults } from '../services/result.interfaces'; -import { Component, getComponents } from '../services/result.service'; -import * as inputs from '../app.input'; import * as core from '@actions/core'; -import { parseSBOM } from '../utils/sbom.utils'; -import { generateTable } from '../utils/markdown.utils'; +import { EXECUTABLE, SCANOSS_SETTINGS } from '../app.input'; +import * as exec from '@actions/exec'; +import { UndeclaredArgumentBuilder } from './argument_builders/undeclared-argument-builder'; +import { ArgumentBuilder } from './argument_builders/argument-builder'; /** * Verifies that all components identified in scanner results are declared in the project's SBOM. @@ -39,74 +38,40 @@ import { generateTable } from '../utils/markdown.utils'; */ export class UndeclaredPolicyCheck extends PolicyCheck { static policyName = 'Undeclared Policy'; - constructor() { + private argumentBuilder: ArgumentBuilder; + constructor(argumentBuilder: ArgumentBuilder = new UndeclaredArgumentBuilder()) { super(`${CHECK_NAME}: ${UndeclaredPolicyCheck.policyName}`); + this.argumentBuilder = argumentBuilder; } - async run(scannerResults: ScannerResults): Promise { + async run(): Promise { + core.info(`Running Undeclared Components Policy Check...`); super.initStatus(); - - const nonDeclaredComponents: Component[] = []; - let declaredComponents: Partial[] = []; - - const comps = getComponents(scannerResults); - - // get declared components - try { - const sbom = await parseSBOM(inputs.SBOM_FILEPATH); - declaredComponents = sbom.components || []; - } catch (e) { - core.info(`Warning on policy check: ${this.checkName}. SBOM file could not be parsed (${inputs.SBOM_FILEPATH})`); - } - - comps.forEach(c => { - if (!declaredComponents.some(component => component.purl === c.purl)) { - nonDeclaredComponents.push(c); - } - }); - - const summary = this.getSummary(nonDeclaredComponents); - let details = this.getDetails(nonDeclaredComponents); - - if (details) { - const { id } = await this.uploadArtifact(details); - if (id) details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); + const args = await this.argumentBuilder.build(); + core.debug(`Args: ${args}`); + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + + const { stdout, stderr, exitCode } = await exec.getExecOutput(EXECUTABLE, args, options); + const summary = stdout; + let details = stderr; + + if (!SCANOSS_SETTINGS) { + core.warning('Undeclared policy is being used with SCANOSS settings disabled'); } - if (nonDeclaredComponents.length === 0) { - return this.success(summary, details); - } else { - return this.reject(summary, details); + if (exitCode === 1) { + await this.success('### :white_check_mark: Policy Pass \n #### Not undeclared components were found', undefined); + return; } - } - - private getSummary(components: Component[]): string { - return components.length === 0 - ? '### :white_check_mark: Policy Pass \n #### Not undeclared components were found' - : `### :x: Policy Fail \n #### ${components.length} undeclared component(s) were found. \n See details for more information.`; - } - - private getDetails(components: Component[]): string | undefined { - if (components.length === 0) return undefined; - - const headers = ['Component', 'Version', 'License']; - const rows: string[][] = []; - - components.forEach(component => { - const licenses = component.licenses.map(l => l.spdxid).join(' - '); - rows.push([component.purl, component.version, licenses]); - }); - - const snippet = JSON.stringify( - components.map(({ purl }) => ({ purl })), - null, - 4 - ); - let content = `### Undeclared components \n ${generateTable(headers, rows)}`; - content += `#### Add the following snippet into your \`sbom.json\` file \n \`\`\`json \n ${snippet} \n \`\`\``; + const { id } = await this.uploadArtifact(details); + core.debug(`Undeclared Artifact ID: ${id}`); + if (id) details = await this.concatPolicyArtifactURLToPolicyCheck(details, id); - return content; + return this.reject(summary, details); } artifactPolicyFileName(): string { diff --git a/src/services/scan.service.ts b/src/services/scan.service.ts index 3ae21e1..781625c 100644 --- a/src/services/scan.service.ts +++ b/src/services/scan.service.ts @@ -28,6 +28,14 @@ import { ScannerResults } from './result.interfaces'; import fs from 'fs'; import * as core from '@actions/core'; import * as path from 'path'; +import { + EXECUTABLE, + OUTPUT_FILEPATH, + SCAN_FILES, + SCANOSS_SETTINGS, + SETTINGS_FILE_PATH, + SKIP_SNIPPETS +} from '../app.input'; const artifact = new DefaultArtifactClient(); @@ -40,21 +48,6 @@ export async function uploadResults(): Promise { } export interface Options { - /** - * Whether SBOM ingestion is enabled. Optional. - */ - sbomEnabled?: boolean; - - /** - * Specifies the SBOM processing type: "identify" or "ignore". Optional. - */ - sbomType?: string; - - /** - * Absolute path to the SBOM file. Required if sbomEnabled is set to true. - */ - sbomFilepath?: string; - /** * Enables scanning for dependencies, utilizing scancode internally. Optional. */ @@ -92,22 +85,67 @@ export interface Options { inputFilepath: string; /** - * Runtime container to perform scan. Default [ghcr.io/scanoss/scanoss-py:v1.15.0] + * Runtime container to perform scan. Default [ghcr.io/scanoss/scanoss-py:v1.19.0] */ runtimeContainer: string; + + /** + * Skips snippet generation. Default [false] + */ + skipSnippets: boolean; + + /** + * Enables or disables file and snippet scanning. Default [true] + */ + scanFiles: boolean; + + /** + * Enables or disables SCANOSS settings. Default [false] + */ + scanossSettings: boolean; + + /** + * SCANOSS Settings file path. Default [scanoss.json] + */ + settingsFilePath: string; } /** - * `ScanService` is a class that wraps the `scanoss.py` Docker image, providing a simplified interface - * for configuring and executing source code scans + * @class ScannerService + * @brief A service class that manages scanning operations using the scanoss-py Docker container + * + * @details + * The ScannerService class provides functionality to scan repositories for: + * - File scanning + * - Dependency analysis + * + * @property {Options} options - Configuration options for the scanner + * @property {string} options.apiKey - API key for SCANOSS service authentication + * @property {string} options.apiUrl - URL endpoint for the SCANOSS service + * @property {boolean} options.sbomEnabled - Flag to enable SBOM generation + * @property {string} options.sbomFilepath - Path to store or read SBOM files + * @property {string} options.sbomType - Type of SBOM format to use + * @property {boolean} options.dependenciesEnabled - Flag to enable dependency scanning + * @property {string} options.outputFilepath - Path for scan results output + * @property {string} options.inputFilepath - Path to the repository to scan + * @property {string} options.runtimeContainer - Docker container image to use + * @property {string} options.dependencyScope - Scope for dependency scanning (prod/dev) + * @property {string} options.dependencyScopeExclude - Dependencies to exclude from scan + * @property {string} options.dependencyScopeInclude - Dependencies to include in scan + * @property {boolean} options.skipSnippets - Flag to skip snippet scanning + * @property {boolean} options.scanFiles - Flag to enable file scanning + * @property {boolean} options.scanossSettings - Flag to enable SCANOSS Settings + * @property {boolean} options.settingsFilePath - Path to settings file + * + * @throws {Error} When required configuration options are missing or invalid + * + * @author [SCANOSS] */ export class ScanService { private options: Options; + private DEFAULT_SETTING_FILE_PATH = 'scanoss.json'; constructor(options?: Options) { this.options = options || { - sbomFilepath: inputs.SBOM_FILEPATH, - sbomType: inputs.SBOM_TYPE, - sbomEnabled: inputs.SBOM_ENABLED, apiKey: inputs.API_KEY, apiUrl: inputs.API_URL, dependenciesEnabled: inputs.DEPENDENCIES_ENABLED, @@ -116,17 +154,59 @@ export class ScanService { dependencyScope: inputs.DEPENDENCIES_SCOPE, dependencyScopeInclude: inputs.DEPENDENCY_SCOPE_INCLUDE, dependencyScopeExclude: inputs.DEPENDENCY_SCOPE_EXCLUDE, - runtimeContainer: inputs.RUNTIME_CONTAINER + runtimeContainer: inputs.RUNTIME_CONTAINER, + skipSnippets: SKIP_SNIPPETS, + scanFiles: SCAN_FILES, + scanossSettings: SCANOSS_SETTINGS, + settingsFilePath: SETTINGS_FILE_PATH }; } + + /** + * @brief Executes the scanning process using a scanoss-py Docker container + * @throws {Error} When Docker command fails or configuration is invalid + * @returns {Promise} The results of the scanning operation + * + * @details + * This method performs the following operations: + * - Validates basic configuration + * - Executes Docker command + * - Uploads results to artifacts + * - Parses and returns results + * + * @note At least one scan option (scanFiles or dependenciesEnabled) must be enabled + */ async scan(): Promise<{ scan: ScannerResults; stdout: string; stderr: string }> { - const command = await this.buildCommand(); - const { stdout, stderr } = await exec.getExecOutput(command, []); + // Check for basic configuration before running the docker container + this.checkBasicConfig(); + + const options = { + failOnStdErr: false, + ignoreReturnCode: true + }; + + const args = await this.buildArgs(); + const { stdout, stderr } = await exec.getExecOutput(EXECUTABLE, args, options); + const scan = await this.parseResult(); return { scan, stdout, stderr }; } - private dependencyScopeCommand(): string { + /** + * @brief Builds the dependency scope command string + * @returns {Array} The formatted dependency scope command + * + * @details + * Handles three possible scope configurations: + * - Dependency scope exclude + * - Dependency scope include + * - Dependency scope (prod/dev) + * + * @throws {Error} When multiple dependency scope filters are set + * + * @note Only one dependency scope filter can be set at a time + */ + private dependencyScopeArgs(): string[] { const { dependencyScopeInclude, dependencyScopeExclude, dependencyScope } = this.options; // Count the number of non-empty values @@ -135,28 +215,102 @@ export class ScanService { ); if (setScopes.length > 1) { - core.setFailed('Only one dependency scope filter can be set'); + core.error('Only one dependency scope filter can be set'); } - if (dependencyScopeExclude !== '') return `--dep-scope-exc ${this.options.dependencyScopeExclude}`; + if (dependencyScopeExclude && dependencyScopeExclude !== '') return ['--dep-scope-exc', dependencyScopeExclude]; - if (dependencyScopeInclude !== '') return `--dep-scope-inc ${this.options.dependencyScopeInclude}`; + if (dependencyScopeInclude && dependencyScopeInclude !== '') return ['--dep-scope-inc', dependencyScopeInclude]; - if (dependencyScope === 'prod') return '--dep-scope prod'; + if (dependencyScope && dependencyScope === 'prod') return ['--dep-scope', 'prod']; - if (dependencyScope === 'dev') return '--dep-scope dev'; + if (dependencyScope && dependencyScope === 'dev') return ['--dep-scope', 'dev']; - return ''; + return []; } - private async buildCommand(): Promise { - return `docker run -v "${this.options.inputFilepath}":"/scanoss" ${this.options.runtimeContainer} scan . - --output ${this.options.outputFilepath} - ${this.options.dependenciesEnabled ? `--dependencies` : ''} - ${this.dependencyScopeCommand()} - ${await this.detectSBOM()} - ${this.options.apiUrl ? `--apiurl ${this.options.apiUrl}` : ''} - ${this.options.apiKey ? `--key ${this.options.apiKey}` : ''}`.replace(/\n/gm, ' '); + /** + * @brief Generates the snippet-related portion of the Docker command + * @returns {Array} The snippet command flag (-S) or empty string + * + * @details + * Returns ["-S"] if snippets should be skipped, empty string otherwise + */ + private buildSnippetArgs(): string[] { + if (!this.options.skipSnippets) return []; + return ['-S']; + } + + /** + * @brief Constructs the dependencies cmd + * @returns {Array} The formatted dependencies command + * + * @details + * Combines dependency scanning options with scope commands. + * Possible return values: + * - [--dependencies-only, ${scopeCmd}]' + * - [--dependencies, ${scopeCmd}] + * - Empty array if no dependencies scanning is needed + */ + private buildDependenciesArgs(): string[] { + const dependencyScopeCmd = this.dependencyScopeArgs(); + if (!this.options.scanFiles && this.options.dependenciesEnabled) { + return ['--dependencies-only', ...dependencyScopeCmd]; + } else if (this.options.dependenciesEnabled) { + return ['--dependencies', ...dependencyScopeCmd]; + } + return []; + } + + /** + * @brief Assembles the complete Docker command string + * @returns {Promise>} The complete Docker command + * + * @details + * Combines all command components: + * - Docker run command with volume mounting + * - Runtime container specification + * - Scan command with output file + * - Dependencies command + * - SBOM detection + * - Snippet command + * - API configuration + * + */ + private async buildArgs(): Promise { + return [ + 'run', + '-v', + `${this.options.inputFilepath}:/scanoss`, + this.options.runtimeContainer, + 'scan', + '.', + '--output', + `./${OUTPUT_FILEPATH}`, + ...this.buildDependenciesArgs(), + ...(await this.detectSBOM()), + ...this.buildSnippetArgs(), + ...(this.options.apiUrl ? ['--apiurl', this.options.apiUrl] : []), + ...(this.options.apiKey ? ['--apiKey', this.options.apiKey.replace(/\n/gm, ' ')] : []) + ]; + } + + /** + * @brief Validates the basic configuration requirements for scanning + * + * @throws {Error} When no scan options are enabled + * + * @details + * This method ensures that at least one of the following scan options is enabled: + * - scanFiles: For scanning source code files + * - dependenciesEnabled: For scanning project dependencies + * + */ + private checkBasicConfig(): void { + if (!this.options.scanFiles && !this.options.dependenciesEnabled) { + core.error(`At least one scan option should be enabled: [scanFiles, dependencyEnabled]`); + } + core.info('Basic scan config is valid'); } /** @@ -171,16 +325,21 @@ export class ScanService { * @returns A command string segment for SBOM ingestion or an empty string if conditions are not met. * @private */ - private async detectSBOM(): Promise { - if (!this.options.sbomEnabled || !this.options.sbomFilepath) return ''; - - try { - await fs.promises.access(this.options.sbomFilepath, fs.constants.F_OK); - return `--${this.options.sbomType} ${this.options.sbomFilepath}`; - } catch (error) { - core.warning('SBOM not found'); - return ''; + private async detectSBOM(): Promise { + // Overrides sbom file if is set + if (this.options.scanossSettings) { + try { + await fs.promises.access(this.options.settingsFilePath, fs.constants.F_OK); + return ['--settings', this.options.settingsFilePath]; + } catch (error: any) { + if (this.options.settingsFilePath === this.DEFAULT_SETTING_FILE_PATH) return []; + core.warning(`SCANOSS settings file not found at '${this.options.settingsFilePath}'. + Please provide a valid SCANOSS settings file path.`); + return []; + } } + // Force scanoss.py to not load the settings.json file + return ['-stf']; } private async parseResult(): Promise {