Compare commits

...

56 Commits

Author SHA1 Message Date
dependabot[bot] 71ec2eb07b Bump lodash from 4.17.21 to 4.17.23 in /test/nested
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-23 20:24:07 +00:00
Lewis Jones 374343effe Merge pull request #6 from actions/weekly-sync-branch-16171136386
Test / test (push) Has been cancelled
Sync Fork with Upstream
2025-07-09 14:49:18 +01:00
github-actions[bot] 5a79ab0fa4 Empty commit to open PR 2025-07-09 13:48:25 +00:00
github-actions[bot] 0c3e582042 Merge upstream:main 2025-07-09 13:48:25 +00:00
Lewis Jones d433c2f467 Merge pull request #126 from advanced-security/ljones140/prep-0.1.0-release
Prepare for release
2025-07-09 14:46:20 +01:00
Lewis Jones 96c59aebfe Bump minor version 2025-07-09 14:42:42 +01:00
Lewis Jones 6dd7b2dc55 Merge pull request #125 from advanced-security/ljones140/remove-manifest-leading-slash
Remove Leading Slash From Root Manifests
2025-07-09 14:40:04 +01:00
Lewis Jones 496691092b remove environment.yml 2025-07-09 13:49:12 +01:00
Lewis Jones e6ad22924a Refactor: improve full scan manifest test 2025-07-09 13:46:43 +01:00
Lewis Jones 0b0b651777 Refactor test. Download latest release 2025-07-09 13:41:17 +01:00
Lewis Jones 914cb6dc5e Remove debug logging 2025-07-09 13:35:28 +01:00
Lewis Jones 28905c6bc0 manifest test 2025-07-09 13:28:41 +01:00
Lewis Jones f89d41905d Add manifest level test 2025-07-09 12:36:47 +01:00
Lewis Jones 4e2fbd91ff Transpiled JS 2025-07-09 12:16:42 +01:00
Lewis Jones 6d25ae13f5 Remove leading slashes from top manifests 2025-07-09 12:16:21 +01:00
Lewis Jones 7d147e8b5f Add nested dirs to test 2025-07-09 11:56:34 +01:00
Lewis Jones 5789c204e4 update test 2025-07-09 11:46:15 +01:00
Lewis Jones 98b7e66125 Transpiled JS 2025-07-09 11:41:22 +01:00
Lewis Jones b2779b0030 Remove leading slash from root manifests
This causes issues with GitHub dependency Graph
2025-07-09 11:39:47 +01:00
Justin Holguín 876b304ec0 Merge pull request #5 from actions/weekly-sync-branch-16062172741
Sync Fork with Upstream
2025-07-03 16:09:54 -07:00
github-actions[bot] 3104f6d51c Empty commit to open PR 2025-07-03 23:08:17 +00:00
github-actions[bot] 5d8c040f29 Merge upstream:main 2025-07-03 23:08:17 +00:00
Justin Holguín 64db6d9d15 Merge pull request #123 from advanced-security/juxtin/prep-007
Prepare for v0.0.7 release
2025-07-02 12:54:29 -07:00
Justin Holguín a44e08867f Prepare for v0.0.7 release 2025-07-02 19:39:44 +00:00
Justin Holguín fc216b239a Merge pull request #121 from advanced-security/juxtin/direct-vs-transitive
Use explicitlyReferencedComponentIds to determine which packages are direct
2025-07-02 12:32:40 -07:00
Justin Holguín 5b2736e4f4 Update dist 2025-07-02 18:40:35 +00:00
Justin Holguín bbe83e8988 Skip self-referrers 2025-07-02 18:40:04 +00:00
Justin Holguín c936885d12 Update dist 2025-06-27 20:28:38 +00:00
Justin Holguín 5f4db12f7b Use explicitlyReferencedComponentIds to mark directs 2025-06-27 20:28:38 +00:00
Lewis Jones 466989c808 Merge pull request #4 from actions/weekly-sync-branch-15774881579
Sync Fork with Upstream
2025-06-20 09:40:55 +01:00
github-actions[bot] 67f3292117 Empty commit to open PR 2025-06-20 08:36:52 +00:00
github-actions[bot] 3f420ae88d Merge upstream:main 2025-06-20 08:36:52 +00:00
Lewis Jones b242ddf67a Merge pull request #120 from advanced-security/ljones140/fix-direct-when-self-referring
Fix Direct Dependencies Marked as Indirect
2025-06-20 09:26:51 +01:00
Lewis Jones 3349f8c032 Generated dist 2025-06-19 15:22:04 +01:00
Lewis Jones 2517c7a607 Add types 2025-06-19 15:21:52 +01:00
Lewis Jones 2efc7af7df Refactor: Extract another method and test with real data 2025-06-19 15:13:55 +01:00
Lewis Jones 6d56d2b42c Don't make self refential referrer as indirect 2025-06-19 12:55:00 +01:00
Lewis Jones 0de0af1352 Remove unnesessary test package incrementation 2025-06-19 12:35:41 +01:00
Lewis Jones 4daccf7142 Ensure tests are testing properly
Don't use mocks
2025-06-19 12:33:31 +01:00
Lewis Jones caa69e181f Extract addPackagesToManifests to unit test
There is a but here we would like to test
2025-06-19 12:14:02 +01:00
Lewis Jones ef571d5a84 Merge pull request #3 from actions/weekly-sync-branch-15680274825
Sync Fork with Upstream
2025-06-16 13:05:03 +01:00
github-actions[bot] 0eb73668fa Empty commit to open PR 2025-06-16 12:04:03 +00:00
github-actions[bot] 7a168cbdc4 Merge upstream:main 2025-06-16 12:04:03 +00:00
Lewis Jones 04aaaf6193 Merge pull request #118 from advanced-security/ljones140/add-snapshot-inputs
Add Snapshot inputs
2025-06-16 13:03:18 +01:00
Lewis Jones 0f3b6aecc6 Generate dist 2025-06-16 11:17:06 +01:00
Lewis Jones 348257c874 Add sha and ref snapshot inputs 2025-06-16 11:15:10 +01:00
Lewis Jones 779e8387fd Add detector inputs
Optional but if any are provided, then all are required
2025-06-16 11:03:28 +01:00
Lewis Jones d5fd67e101 Merge pull request #2 from actions/weekly-sync-branch-15612676798
Sync Fork with Upstream
2025-06-12 15:02:36 +01:00
github-actions[bot] 27e6d82755 Empty commit to open PR 2025-06-12 14:01:47 +00:00
github-actions[bot] 3d11e5a0f7 Merge upstream:main 2025-06-12 14:01:47 +00:00
Lewis Jones e0dcc85667 Merge pull request #117 from actions/ljones140/clean-detector-categories-pr
Add DetectorCategories input So we can run by ecosystem
2025-06-12 13:26:39 +01:00
Lewis Jones 4f5a06217d Remove examples
As not confirmed they are correct.

For example PIp doesn't work but Python does
2025-06-12 12:23:38 +01:00
Lewis Jones 81fde650c2 Add new input to readme 2025-06-12 12:10:17 +01:00
Lewis Jones 786fb5fe93 dist generated code 2025-06-12 12:10:06 +01:00
Lewis Jones 550b6f27ed Pass detectorCategories
As we want to use for specific ecosystems.
2025-06-12 12:09:54 +01:00
Lewis Jones 51ef6b3995 Merge pull request #1 from actions/ljones140/setup-fork
Setup fork Codeowners and sync
2025-06-10 15:35:24 +01:00
12 changed files with 593 additions and 125 deletions
+8 -7
View File
@@ -1,6 +1,6 @@
# Component detection dependency submission action
This GitHub Action runs the [microsoft/component-detection](https://github.com/microsoft/component-detection) library to automate dependency extraction at build time. It uses a combination of static and dynamic scanning to build a dependency tree and then uploads that to GitHub's dependency graph via the dependency submission API. This gives you more accurate Dependabot alerts, and support for a bunch of additional ecosystems.
This GitHub Action runs the [microsoft/component-detection](https://github.com/microsoft/component-detection) library to automate dependency extraction at build time. It uses a combination of static and dynamic scanning to build a dependency tree and then uploads that to GitHub's dependency graph via the dependency submission API. This gives you more accurate Dependabot alerts, and support for a bunch of additional ecosystems.
### Example workflow
@@ -12,7 +12,7 @@ on:
workflow_dispatch:
push:
permissions:
permissions:
id-token: write
contents: write
@@ -21,19 +21,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Component detection
- name: Component detection
uses: advanced-security/component-detection-dependency-submission-action@v0.0.3
```
```
### Configuration options
| Parameter | Description | Example |
| --- | --- | --- |
| Parameter | Description | Example |
| --- | --- | --- |
filePath | The path to the directory containing the environment files to upload. Defaults to Actions working directory. | `'.'`
directoryExclusionList | Filters out specific directories following a minimatch pattern. | `test`
detectorArgs | Comma separated list of properties that can affect the detectors execution, like EnableIfDefaultOff that allows a specific detector that is in beta to run, the format for this property is DetectorId=EnableIfDefaultOff, for example Pip=EnableIfDefaultOff. | `Pip=EnableIfDefaultOff`
dockerImagesToScan |Comma separated list of docker image names or hashes to execute container scanning on | ubuntu:16.04,56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab |
dockerImagesToScan |Comma separated list of docker image names or hashes to execute container scanning on | ubuntu:16.04,56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab |
detectorsFilter | A comma separated list with the identifiers of the specific detectors to be used. | `Pip, RustCrateDetector`
detectorsCategories | A comma separated list with the categories of components that are going to be scanned. The detectors that are going to run are the ones that belongs to the categories. | `NuGet,Npm`
correlator | An optional identifier to distinguish between multiple dependency snapshots of the same type. Defaults to the [job_id](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_id) of the current job | `csharp-backend`
For more information: https://github.com/microsoft/component-detection
+20 -3
View File
@@ -5,7 +5,7 @@ inputs:
description: "GitHub Personal Access Token (PAT). Defaults to PAT provided by Actions runner."
required: false
default: ${{ github.token }}
filePath:
filePath:
description: 'The path to the directory containing the environment files to upload. Defaults to Actions working directory.'
required: false
default: '.'
@@ -18,12 +18,29 @@ inputs:
dockerImagesToScan:
description: 'Comma separated list of docker image names or hashes to execute container scanning on, ex: ubuntu:16.04,56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab'
required: false
detectorsFilter:
detectorsFilter:
description: 'A comma separated list with the identifiers of the specific detectors to be used. This is meant to be used for testing purposes only.'
required: false
detectorsCategories:
description: 'A comma separated list with the categories of components that are going to be scanned. The detectors that are going to run are the ones that belongs to the categories. The possible values are: Npm, NuGet, Maven, RubyGems, Cargo, Pip, GoMod, CocoaPods, Linux.'
required: false
correlator:
description: 'An optional identifier to distinguish between multiple dependency snapshots of the same type.'
type: string
required: false
detector-name:
description: 'The name of the detector. If provided, detector-version and detector-url must also be provided.'
required: false
detector-version:
description: 'The version of the detector. If provided, detector-name and detector-url must also be provided.'
required: false
detector-url:
description: 'The URL of the detector. If provided, detector-name and detector-version must also be provided.'
required: false
snapshot-sha:
description: 'The SHA of the commit to associate with the snapshot. If provided, snapshot-ref must also be provided.'
required: false
snapshot-ref:
description: 'The Git reference to associate with the snapshot. If provided, snapshot-sha must also be provided.'
required: false
runs:
using: 'node20'
+177 -1
View File
@@ -1,4 +1,4 @@
import ComponentDetection from "./componentDetection";
import ComponentDetection, { DependencyGraphs } from "./componentDetection";
import fs from "fs";
test("Downloads CLI", async () => {
@@ -68,3 +68,179 @@ describe("ComponentDetection.makePackageUrl", () => {
expect(packageUrl).toBe("");
});
});
describe("ComponentDetection.processComponentsToManifests", () => {
test("adds package as direct dependency when it is listed as an explicitlyReferencedComponentIds", () => {
const componentsFound = [
{
component: {
name: "test-package",
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "test-package",
Version: "1.0.0"
},
id: "test-package 1.0.0 - npm"
},
isDevelopmentDependency: false,
topLevelReferrers: [], // Empty = direct dependency
locationsFoundAt: ["package.json"]
}
];
const dependencyGraphs: DependencyGraphs = {
"package.json": {
graph: { "test-package": null },
explicitlyReferencedComponentIds: ["test-package 1.0.0 - npm"],
developmentDependencies: [],
dependencies: []
}
};
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
expect(manifests).toHaveLength(1);
expect(manifests[0].name).toBe("package.json");
expect(manifests[0].directDependencies()).toHaveLength(1);
expect(manifests[0].indirectDependencies()).toHaveLength(0);
expect(manifests[0].countDependencies()).toBe(1);
});
test("adds package as indirect dependency when it is not in explicitlyReferencedComponentIds", () => {
const componentsFound = [
{
component: {
name: "test-package",
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "test-package",
Version: "1.0.0"
},
id: "test-package 1.0.0 - npm"
},
isDevelopmentDependency: false,
topLevelReferrers: [
{
name: "parent-package",
version: "1.0.0",
packageUrl: {
Scheme: "pkg",
Type: "npm",
Name: "parent-package",
Version: "1.0.0"
}
}
],
locationsFoundAt: ["package.json"]
}
];
const dependencyGraphs: DependencyGraphs = {
"package.json": {
graph: { "parent-package": null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
}
};
const manifests = ComponentDetection.processComponentsToManifests(componentsFound, dependencyGraphs);
expect(manifests).toHaveLength(1);
expect(manifests[0].name).toBe("package.json");
expect(manifests[0].directDependencies()).toHaveLength(0);
expect(manifests[0].indirectDependencies()).toHaveLength(1);
expect(manifests[0].countDependencies()).toBe(1);
});
});
describe('normalizeDependencyGraphPaths', () => {
test('converts absolute paths to relative paths based on filePath input', () => {
// Simulate a repo at /repo and a scan root at /repo/packages
const fakeCwd = '/workspaces';
const filePathInput = 'my-super-cool-repo';
const absBase = '/workspaces/my-super-cool-repo';
const dependencyGraphs: DependencyGraphs = {
'/workspaces/my-super-cool-repo/a/package.json': {
graph: { 'foo': null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
},
'/workspaces/my-super-cool-repo/b/package.json': {
graph: { 'bar': null },
explicitlyReferencedComponentIds: [],
developmentDependencies: [],
dependencies: []
}
};
// Patch process.cwd for this test
const originalCwd = process.cwd;
(process as any).cwd = () => fakeCwd;
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, filePathInput);
// Restore process.cwd
(process as any).cwd = originalCwd;
expect(Object.keys(normalized)).toContain('a/package.json');
expect(Object.keys(normalized)).toContain('b/package.json');
expect(normalized['a/package.json'].graph).toEqual({ 'foo': null });
expect(normalized['b/package.json'].graph).toEqual({ 'bar': null });
});
});
describe('normalizeDependencyGraphPaths with real output.json', () => {
test('converts absolute paths in output.json to relative paths using current cwd and filePath', () => {
const output = JSON.parse(fs.readFileSync('./output.json', 'utf8'));
const dependencyGraphs = output.dependencyGraphs;
// Use the same filePath as the action default (".")
const normalized = ComponentDetection.normalizeDependencyGraphPaths(dependencyGraphs, 'test');
// Should contain root level manifests without leading slashes
expect(Object.keys(normalized)).toContain('package.json');
expect(Object.keys(normalized)).toContain('package-lock.json');
// Should contain nested manifests with relative paths (no leading slashes)
expect(Object.keys(normalized)).toContain('nested/package.json');
expect(Object.keys(normalized)).toContain('nested/package-lock.json');
// All keys should be relative paths without leading slashes
for (const key of Object.keys(normalized)) {
expect(key.startsWith('/')).toBe(false); // No leading slashes
expect(key).not.toMatch(/^\w:\\|^\/\/|^\.{1,2}\//); // Not windows absolute, not network, not relative
}
});
});
test('full action scan creates manifests with correct names and file source locations', async () => {
await ComponentDetection.downloadLatestRelease();
const manifests = await ComponentDetection.scanAndGetManifests('./test');
expect(manifests).toBeDefined();
expect(manifests!.length).toBeGreaterThan(0);
for (const manifest of manifests!) {
expect(manifest.name.startsWith('/')).toBe(false);
}
const expectedManifestNames = [
'package.json',
'package-lock.json',
'nested/package.json',
'nested/package-lock.json',
];
const manifestsByName = manifests!.reduce((acc, manifest) => {
acc[manifest.name] = manifest;
return acc;
}, {} as Record<string, any>);
for (const expectedName of expectedManifestNames) {
const manifest = manifestsByName[expectedName];
expect(manifest).toBeDefined();
expect(manifest.name).toBe(expectedName);
expect(manifest.file?.source_location).toBe(expectedName);
}
}, 15000);
+109 -22
View File
@@ -7,7 +7,7 @@ import {
Package,
Snapshot,
Manifest,
submitSnapshot
submitSnapshot,
} from '@github/dependency-submission-toolkit'
import fetch from 'cross-fetch'
import tar from 'tar'
@@ -16,13 +16,14 @@ import * as exec from '@actions/exec';
import dotenv from 'dotenv'
import { Context } from '@actions/github/lib/context'
import { unmockedModulePathPatterns } from './jest.config'
import path from 'path';
dotenv.config();
export default class ComponentDetection {
public static componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection';
public static outputPath = './output.json';
// This is the default entry point for this class.
// This is the default entry point for this class.
static async scanAndGetManifests(path: string): Promise<Manifest[] | undefined> {
await this.downloadLatestRelease();
await this.runComponentDetection(path);
@@ -61,20 +62,25 @@ export default class ComponentDetection {
parameters += (core.getInput('directoryExclusionList')) ? ` --DirectoryExclusionList ${core.getInput('directoryExclusionList')}` : "";
parameters += (core.getInput('detectorArgs')) ? ` --DetectorArgs ${core.getInput('detectorArgs')}` : "";
parameters += (core.getInput('detectorsFilter')) ? ` --DetectorsFilter ${core.getInput('detectorsFilter')}` : "";
parameters += (core.getInput('detectorsCategories')) ? ` --DetectorCategories ${core.getInput('detectorsCategories')}` : "";
parameters += (core.getInput('dockerImagesToScan')) ? ` --DockerImagesToScan ${core.getInput('dockerImagesToScan')}` : "";
return parameters;
}
public static async getManifestsFromResults(): Promise<Manifest[] | undefined> {
core.info("Getting manifests from results");
const results = await fs.readFileSync(this.outputPath, 'utf8');
var json: any = JSON.parse(results);
let dependencyGraphs: DependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
}
public static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[] {
// Parse the result file and add the packages to the package cache
const packageCache = new PackageCache();
const packages: Array<ComponentDetectionPackage> = [];
const results = await fs.readFileSync(this.outputPath, 'utf8');
var json: any = JSON.parse(results);
json.componentsFound.forEach(async (component: any) => {
componentsFound.forEach(async (component: any) => {
// Skip components without packageUrl
if (!component.component.packageUrl) {
core.debug(`Skipping component detected without packageUrl: ${JSON.stringify({
@@ -86,7 +92,7 @@ export default class ComponentDetection {
}
const packageUrl = ComponentDetection.makePackageUrl(component.component.packageUrl);
// Skip if the packageUrl is empty (indicates an invalid or missing packageUrl)
if (!packageUrl) {
core.debug(`Skipping component with invalid packageUrl: ${component.component.id}`);
@@ -110,17 +116,22 @@ export default class ComponentDetection {
core.debug(`Skipping referrer without packageUrl for component: ${pkg.id}`);
return;
}
const referrerUrl = ComponentDetection.makePackageUrl(referrer.packageUrl);
referrer.packageUrlString = referrerUrl
// Skip if the generated packageUrl is empty
if (!referrerUrl) {
core.debug(`Skipping referrer with invalid packageUrl for component: ${pkg.id}`);
return;
}
try {
const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage === pkg) {
core.debug(`Skipping self-reference for package: ${pkg.id}`);
return; // Skip self-references
}
if (referrerPackage) {
referrerPackage.dependsOn(pkg);
}
@@ -134,20 +145,46 @@ export default class ComponentDetection {
const manifests: Array<Manifest> = [];
// Check the locationsFoundAt for every package and add each as a manifest
packages.forEach(async (pkg: ComponentDetectionPackage) => {
pkg.locationsFoundAt.forEach(async (location: any) => {
if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
const manifest = new Manifest(location, location);
this.addPackagesToManifests(packages, manifests, dependencyGraphs);
return manifests;
}
private static addPackagesToManifests(packages: Array<ComponentDetectionPackage>, manifests: Array<Manifest>, dependencyGraphs: DependencyGraphs): void {
packages.forEach((pkg: ComponentDetectionPackage) => {
pkg.locationsFoundAt.forEach((location: any) => {
// Use the normalized path (remove leading slash if present)
const normalizedLocation = location.startsWith('/') ? location.substring(1) : location;
if (!manifests.find((manifest: Manifest) => manifest.name == normalizedLocation)) {
const manifest = new Manifest(normalizedLocation, normalizedLocation);
manifests.push(manifest);
}
if (pkg.topLevelReferrers.length == 0) {
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
const depGraphEntry = dependencyGraphs[normalizedLocation];
if (!depGraphEntry) {
core.warning(`No dependency graph entry found for manifest location: ${normalizedLocation}`);
return; // Skip this location if not found in dependencyGraphs
}
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
if (directDependencies.includes(pkg.id)) {
manifests
.find((manifest: Manifest) => manifest.name == normalizedLocation)
?.addDirectDependency(
pkg,
ComponentDetection.getDependencyScope(pkg)
);
} else {
manifests.find((manifest: Manifest) => manifest.name == location)?.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
manifests
.find((manifest: Manifest) => manifest.name == normalizedLocation)
?.addIndirectDependency(
pkg,
ComponentDetection.getDependencyScope(pkg)
);
}
});
});
return manifests;
}
private static getDependencyScope(pkg: ComponentDetectionPackage) {
@@ -195,10 +232,10 @@ export default class ComponentDetection {
private static async getLatestReleaseURL(): Promise<string> {
let githubToken = core.getInput('token') || process.env.GITHUB_TOKEN || "";
const githubAPIURL = 'https://api.github.com'
const githubAPIURL = 'https://api.github.com'
let ghesMode = github.context.apiUrl != githubAPIURL;
// If the we're running in GHES, then use an empty string as the token
// If the we're running in GHES, then use an empty string as the token
if (ghesMode) {
githubToken = "";
}
@@ -213,7 +250,7 @@ export default class ComponentDetection {
const repo = "component-detection";
core.debug("Attempting to download latest release from " + githubAPIURL);
try {
try {
const latestRelease = await octokit.request("GET /repos/{owner}/{repo}/releases/latest", {owner, repo});
var downloadURL: string = "";
@@ -229,19 +266,69 @@ export default class ComponentDetection {
core.error(error);
core.debug(error.message);
core.debug(error.stack);
throw new Error("Failed to download latest release");
throw new Error("Failed to download latest release");
}
}
/**
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
* @param dependencyGraphs The DependencyGraphs object to normalize.
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
* @returns A new DependencyGraphs object with relative path keys.
*/
public static normalizeDependencyGraphPaths(
dependencyGraphs: DependencyGraphs,
filePathInput: string
): DependencyGraphs {
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
const baseDir = path.resolve(process.cwd(), filePathInput);
const normalized: DependencyGraphs = {};
for (const absPath in dependencyGraphs) {
// Make the path relative to the baseDir
let relPath = path.relative(baseDir, absPath).replace(/\\/g, '/');
normalized[relPath] = dependencyGraphs[absPath];
}
return normalized;
}
}
class ComponentDetectionPackage extends Package {
public packageUrlString: string;
constructor(packageUrl: string, public id: string, public isDevelopmentDependency: boolean, public topLevelReferrers: [],
public locationsFoundAt: [], public containerDetailIds: [], public containerLayerIds: []) {
super(packageUrl);
this.packageUrlString = packageUrl;
}
}
/**
* Types for the dependencyGraphs section of output.json
*/
export type DependencyGraph = {
/**
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
*/
graph: Record<string, string[] | null>;
/**
* Explicitly referenced component IDs
*/
explicitlyReferencedComponentIds: string[];
/**
* Development dependencies
*/
developmentDependencies: string[];
/**
* Regular dependencies
*/
dependencies: string[];
};
/**
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
*/
export type DependencyGraphs = Record<string, DependencyGraph>;
+34
View File
@@ -7,7 +7,41 @@ export default class ComponentDetection {
static runComponentDetection(path: string): Promise<void>;
private static getComponentDetectionParameters;
static getManifestsFromResults(): Promise<Manifest[] | undefined>;
static processComponentsToManifests(componentsFound: any[], dependencyGraphs: DependencyGraphs): Manifest[];
private static addPackagesToManifests;
private static getDependencyScope;
static makePackageUrl(packageUrlJson: any): string;
private static getLatestReleaseURL;
/**
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
* @param dependencyGraphs The DependencyGraphs object to normalize.
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
* @returns A new DependencyGraphs object with relative path keys.
*/
static normalizeDependencyGraphPaths(dependencyGraphs: DependencyGraphs, filePathInput: string): DependencyGraphs;
}
/**
* Types for the dependencyGraphs section of output.json
*/
export type DependencyGraph = {
/**
* The dependency graph: keys are component IDs, values are either null (no dependencies) or an array of component IDs (dependencies)
*/
graph: Record<string, string[] | null>;
/**
* Explicitly referenced component IDs
*/
explicitlyReferencedComponentIds: string[];
/**
* Development dependencies
*/
developmentDependencies: string[];
/**
* Regular dependencies
*/
dependencies: string[];
};
/**
* The top-level dependencyGraphs object: keys are manifest file paths, values are DependencyGraph objects
*/
export type DependencyGraphs = Record<string, DependencyGraph>;
Generated Vendored
+146 -76
View File
@@ -36002,9 +36002,10 @@ const cross_fetch_1 = __importDefault(__nccwpck_require__(3304));
const fs_1 = __importDefault(__nccwpck_require__(9896));
const exec = __importStar(__nccwpck_require__(5236));
const dotenv_1 = __importDefault(__nccwpck_require__(8889));
const path_1 = __importDefault(__nccwpck_require__(6928));
dotenv_1.default.config();
class ComponentDetection {
// This is the default entry point for this class.
// This is the default entry point for this class.
static scanAndGetManifests(path) {
return __awaiter(this, void 0, void 0, function* () {
yield this.downloadLatestRelease();
@@ -36047,84 +36048,107 @@ class ComponentDetection {
parameters += (core.getInput('directoryExclusionList')) ? ` --DirectoryExclusionList ${core.getInput('directoryExclusionList')}` : "";
parameters += (core.getInput('detectorArgs')) ? ` --DetectorArgs ${core.getInput('detectorArgs')}` : "";
parameters += (core.getInput('detectorsFilter')) ? ` --DetectorsFilter ${core.getInput('detectorsFilter')}` : "";
parameters += (core.getInput('detectorsCategories')) ? ` --DetectorCategories ${core.getInput('detectorsCategories')}` : "";
parameters += (core.getInput('dockerImagesToScan')) ? ` --DockerImagesToScan ${core.getInput('dockerImagesToScan')}` : "";
return parameters;
}
static getManifestsFromResults() {
return __awaiter(this, void 0, void 0, function* () {
core.info("Getting manifests from results");
// Parse the result file and add the packages to the package cache
const packageCache = new dependency_submission_toolkit_1.PackageCache();
const packages = [];
const results = yield fs_1.default.readFileSync(this.outputPath, 'utf8');
var json = JSON.parse(results);
json.componentsFound.forEach((component) => __awaiter(this, void 0, void 0, function* () {
// Skip components without packageUrl
if (!component.component.packageUrl) {
core.debug(`Skipping component detected without packageUrl: ${JSON.stringify({
id: component.component.id,
name: component.component.name || 'unnamed',
type: component.component.type || 'unknown'
}, null, 2)}`);
let dependencyGraphs = this.normalizeDependencyGraphPaths(json.dependencyGraphs, core.getInput('filePath'));
return this.processComponentsToManifests(json.componentsFound, dependencyGraphs);
});
}
static processComponentsToManifests(componentsFound, dependencyGraphs) {
// Parse the result file and add the packages to the package cache
const packageCache = new dependency_submission_toolkit_1.PackageCache();
const packages = [];
componentsFound.forEach((component) => __awaiter(this, void 0, void 0, function* () {
// Skip components without packageUrl
if (!component.component.packageUrl) {
core.debug(`Skipping component detected without packageUrl: ${JSON.stringify({
id: component.component.id,
name: component.component.name || 'unnamed',
type: component.component.type || 'unknown'
}, null, 2)}`);
return;
}
const packageUrl = ComponentDetection.makePackageUrl(component.component.packageUrl);
// Skip if the packageUrl is empty (indicates an invalid or missing packageUrl)
if (!packageUrl) {
core.debug(`Skipping component with invalid packageUrl: ${component.component.id}`);
return;
}
if (!packageCache.hasPackage(packageUrl)) {
const pkg = new ComponentDetectionPackage(packageUrl, component.component.id, component.isDevelopmentDependency, component.topLevelReferrers, component.locationsFoundAt, component.containerDetailIds, component.containerLayerIds);
packageCache.addPackage(pkg);
packages.push(pkg);
}
}));
// Set the transitive dependencies
core.debug("Sorting out transitive dependencies");
packages.forEach((pkg) => __awaiter(this, void 0, void 0, function* () {
pkg.topLevelReferrers.forEach((referrer) => __awaiter(this, void 0, void 0, function* () {
// Skip if referrer doesn't have a valid packageUrl
if (!referrer.packageUrl) {
core.debug(`Skipping referrer without packageUrl for component: ${pkg.id}`);
return;
}
const packageUrl = ComponentDetection.makePackageUrl(component.component.packageUrl);
// Skip if the packageUrl is empty (indicates an invalid or missing packageUrl)
if (!packageUrl) {
core.debug(`Skipping component with invalid packageUrl: ${component.component.id}`);
const referrerUrl = ComponentDetection.makePackageUrl(referrer.packageUrl);
referrer.packageUrlString = referrerUrl;
// Skip if the generated packageUrl is empty
if (!referrerUrl) {
core.debug(`Skipping referrer with invalid packageUrl for component: ${pkg.id}`);
return;
}
if (!packageCache.hasPackage(packageUrl)) {
const pkg = new ComponentDetectionPackage(packageUrl, component.component.id, component.isDevelopmentDependency, component.topLevelReferrers, component.locationsFoundAt, component.containerDetailIds, component.containerLayerIds);
packageCache.addPackage(pkg);
packages.push(pkg);
try {
const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage === pkg) {
core.debug(`Skipping self-reference for package: ${pkg.id}`);
return; // Skip self-references
}
if (referrerPackage) {
referrerPackage.dependsOn(pkg);
}
}
catch (error) {
core.debug(`Error looking up referrer package: ${error}`);
}
}));
// Set the transitive dependencies
core.debug("Sorting out transitive dependencies");
packages.forEach((pkg) => __awaiter(this, void 0, void 0, function* () {
pkg.topLevelReferrers.forEach((referrer) => __awaiter(this, void 0, void 0, function* () {
// Skip if referrer doesn't have a valid packageUrl
if (!referrer.packageUrl) {
core.debug(`Skipping referrer without packageUrl for component: ${pkg.id}`);
return;
}
const referrerUrl = ComponentDetection.makePackageUrl(referrer.packageUrl);
// Skip if the generated packageUrl is empty
if (!referrerUrl) {
core.debug(`Skipping referrer with invalid packageUrl for component: ${pkg.id}`);
return;
}
try {
const referrerPackage = packageCache.lookupPackage(referrerUrl);
if (referrerPackage) {
referrerPackage.dependsOn(pkg);
}
}
catch (error) {
core.debug(`Error looking up referrer package: ${error}`);
}
}));
}));
// Create manifests
const manifests = [];
// Check the locationsFoundAt for every package and add each as a manifest
packages.forEach((pkg) => __awaiter(this, void 0, void 0, function* () {
pkg.locationsFoundAt.forEach((location) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!manifests.find((manifest) => manifest.name == location)) {
const manifest = new dependency_submission_toolkit_1.Manifest(location, location);
manifests.push(manifest);
}
if (pkg.topLevelReferrers.length == 0) {
(_a = manifests.find((manifest) => manifest.name == location)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
}
else {
(_b = manifests.find((manifest) => manifest.name == location)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
}
}));
}));
return manifests;
}));
// Create manifests
const manifests = [];
// Check the locationsFoundAt for every package and add each as a manifest
this.addPackagesToManifests(packages, manifests, dependencyGraphs);
return manifests;
}
static addPackagesToManifests(packages, manifests, dependencyGraphs) {
packages.forEach((pkg) => {
pkg.locationsFoundAt.forEach((location) => {
var _a, _b;
// Use the normalized path (remove leading slash if present)
const normalizedLocation = location.startsWith('/') ? location.substring(1) : location;
if (!manifests.find((manifest) => manifest.name == normalizedLocation)) {
const manifest = new dependency_submission_toolkit_1.Manifest(normalizedLocation, normalizedLocation);
manifests.push(manifest);
}
const depGraphEntry = dependencyGraphs[normalizedLocation];
if (!depGraphEntry) {
core.warning(`No dependency graph entry found for manifest location: ${normalizedLocation}`);
return; // Skip this location if not found in dependencyGraphs
}
const directDependencies = depGraphEntry.explicitlyReferencedComponentIds;
if (directDependencies.includes(pkg.id)) {
(_a = manifests
.find((manifest) => manifest.name == normalizedLocation)) === null || _a === void 0 ? void 0 : _a.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
}
else {
(_b = manifests
.find((manifest) => manifest.name == normalizedLocation)) === null || _b === void 0 ? void 0 : _b.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
}
});
});
}
static getDependencyScope(pkg) {
@@ -36170,7 +36194,7 @@ class ComponentDetection {
let githubToken = core.getInput('token') || process.env.GITHUB_TOKEN || "";
const githubAPIURL = 'https://api.github.com';
let ghesMode = github.context.apiUrl != githubAPIURL;
// If the we're running in GHES, then use an empty string as the token
// If the we're running in GHES, then use an empty string as the token
if (ghesMode) {
githubToken = "";
}
@@ -36202,6 +36226,23 @@ class ComponentDetection {
}
});
}
/**
* Normalizes the keys of a DependencyGraphs object to be relative paths from the resolved filePath input.
* @param dependencyGraphs The DependencyGraphs object to normalize.
* @param filePathInput The filePath input (relative or absolute) from the action configuration.
* @returns A new DependencyGraphs object with relative path keys.
*/
static normalizeDependencyGraphPaths(dependencyGraphs, filePathInput) {
// Resolve the base directory from filePathInput (relative to cwd if not absolute)
const baseDir = path_1.default.resolve(process.cwd(), filePathInput);
const normalized = {};
for (const absPath in dependencyGraphs) {
// Make the path relative to the baseDir
let relPath = path_1.default.relative(baseDir, absPath).replace(/\\/g, '/');
normalized[relPath] = dependencyGraphs[absPath];
}
return normalized;
}
}
exports["default"] = ComponentDetection;
ComponentDetection.componentDetectionPath = process.platform === "win32" ? './component-detection.exe' : './component-detection';
@@ -36215,6 +36256,7 @@ class ComponentDetectionPackage extends dependency_submission_toolkit_1.Package
this.locationsFoundAt = locationsFoundAt;
this.containerDetailIds = containerDetailIds;
this.containerLayerIds = containerLayerIds;
this.packageUrlString = packageUrl;
}
}
@@ -36266,23 +36308,51 @@ const github = __importStar(__nccwpck_require__(3228));
const dependency_submission_toolkit_1 = __nccwpck_require__(3323);
const componentDetection_1 = __importDefault(__nccwpck_require__(3202));
function run() {
var _a;
var _a, _b, _c, _d, _e, _f;
return __awaiter(this, void 0, void 0, function* () {
let manifests = yield componentDetection_1.default.scanAndGetManifests(core.getInput('filePath'));
const correlatorInput = ((_a = core.getInput('correlator')) === null || _a === void 0 ? void 0 : _a.trim()) || github.context.job;
let snapshot = new dependency_submission_toolkit_1.Snapshot({
name: "Component Detection",
version: "0.0.1",
url: "https://github.com/advanced-security/component-detection-dependency-submission-action",
}, github.context, {
let manifests = yield componentDetection_1.default.scanAndGetManifests(core.getInput("filePath"));
const correlatorInput = ((_a = core.getInput("correlator")) === null || _a === void 0 ? void 0 : _a.trim()) || github.context.job;
// Get detector configuration inputs
const detectorName = (_b = core.getInput("detector-name")) === null || _b === void 0 ? void 0 : _b.trim();
const detectorVersion = (_c = core.getInput("detector-version")) === null || _c === void 0 ? void 0 : _c.trim();
const detectorUrl = (_d = core.getInput("detector-url")) === null || _d === void 0 ? void 0 : _d.trim();
// Validate that if any detector config is provided, all must be provided
const hasAnyDetectorInput = detectorName || detectorVersion || detectorUrl;
const hasAllDetectorInputs = detectorName && detectorVersion && detectorUrl;
if (hasAnyDetectorInput && !hasAllDetectorInputs) {
core.setFailed("If any detector configuration is provided (detector-name, detector-version, detector-url), all three must be provided.");
return;
}
// Use provided detector config or defaults
const detector = hasAllDetectorInputs
? {
name: detectorName,
version: detectorVersion,
url: detectorUrl,
}
: {
name: "Component Detection",
version: "0.0.1",
url: "https://github.com/advanced-security/component-detection-dependency-submission-action",
};
let snapshot = new dependency_submission_toolkit_1.Snapshot(detector, github.context, {
correlator: correlatorInput,
id: github.context.runId.toString()
id: github.context.runId.toString(),
});
core.debug(`Manifests: ${manifests === null || manifests === void 0 ? void 0 : manifests.length}`);
manifests === null || manifests === void 0 ? void 0 : manifests.forEach(manifest => {
manifests === null || manifests === void 0 ? void 0 : manifests.forEach((manifest) => {
core.debug(`Manifest: ${JSON.stringify(manifest)}`);
snapshot.addManifest(manifest);
});
// Override snapshot ref and sha if provided
const snapshotSha = (_e = core.getInput("snapshot-sha")) === null || _e === void 0 ? void 0 : _e.trim();
const snapshotRef = (_f = core.getInput("snapshot-ref")) === null || _f === void 0 ? void 0 : _f.trim();
if (snapshotSha) {
snapshot.sha = snapshotSha;
}
if (snapshotRef) {
snapshot.ref = snapshotRef;
}
(0, dependency_submission_toolkit_1.submitSnapshot)(snapshot);
});
}
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+50 -12
View File
@@ -13,27 +13,65 @@ import {
import ComponentDetection from './componentDetection';
async function run() {
let manifests = await ComponentDetection.scanAndGetManifests(core.getInput('filePath'));
const correlatorInput = core.getInput('correlator')?.trim() || github.context.job;
let snapshot = new Snapshot({
name: "Component Detection",
version: "0.0.1",
url: "https://github.com/advanced-security/component-detection-dependency-submission-action",
},
github.context,
{
let manifests = await ComponentDetection.scanAndGetManifests(
core.getInput("filePath")
);
const correlatorInput =
core.getInput("correlator")?.trim() || github.context.job;
// Get detector configuration inputs
const detectorName = core.getInput("detector-name")?.trim();
const detectorVersion = core.getInput("detector-version")?.trim();
const detectorUrl = core.getInput("detector-url")?.trim();
// Validate that if any detector config is provided, all must be provided
const hasAnyDetectorInput = detectorName || detectorVersion || detectorUrl;
const hasAllDetectorInputs = detectorName && detectorVersion && detectorUrl;
if (hasAnyDetectorInput && !hasAllDetectorInputs) {
core.setFailed(
"If any detector configuration is provided (detector-name, detector-version, detector-url), all three must be provided."
);
return;
}
// Use provided detector config or defaults
const detector = hasAllDetectorInputs
? {
name: detectorName,
version: detectorVersion,
url: detectorUrl,
}
: {
name: "Component Detection",
version: "0.0.1",
url: "https://github.com/advanced-security/component-detection-dependency-submission-action",
};
let snapshot = new Snapshot(detector, github.context, {
correlator: correlatorInput,
id: github.context.runId.toString()
id: github.context.runId.toString(),
});
core.debug(`Manifests: ${manifests?.length}`);
manifests?.forEach(manifest => {
manifests?.forEach((manifest) => {
core.debug(`Manifest: ${JSON.stringify(manifest)}`);
snapshot.addManifest(manifest);
});
// Override snapshot ref and sha if provided
const snapshotSha = core.getInput("snapshot-sha")?.trim();
const snapshotRef = core.getInput("snapshot-ref")?.trim();
if (snapshotSha) {
snapshot.sha = snapshotSha;
}
if (snapshotRef) {
snapshot.ref = snapshotRef;
}
submitSnapshot(snapshot);
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "component-detection-action",
"version": "1.0.0",
"version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "component-detection-action",
"version": "1.0.0",
"version": "0.0.7",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "component-detection-action",
"version": "1.0.0",
"version": "0.1.0",
"description": "Component detection action",
"main": "dist/index.js",
"type": "module",
+33
View File
@@ -0,0 +1,33 @@
{
"name": "nested-test-package",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nested-test-package",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.23"
},
"devDependencies": {
"jest": "^29.0.0"
}
},
"node_modules/jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"bin": {
"jest": "bin/jest.js"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "nested-test-package",
"version": "1.0.0",
"description": "A nested test package for component detection testing",
"main": "index.js",
"dependencies": {
"lodash": "^4.17.23"
},
"devDependencies": {
"jest": "^29.0.0"
}
}