Skip to content

Commit 3685f28

Browse files
authored
Merge pull request #5 from jhutchings1/develop
Convert to use component-detection library
2 parents e2b34c6 + ffddd79 commit 3685f28

19 files changed

+16715
-16402
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ typings/
6565

6666
# next.js build output
6767
.next
68+
69+
# Output from scanning
70+
output.json

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
# Conda dependency submission action
2-
3-
This repository scans Conda environment.yaml files and uploads the results to the dependency graph. While GitHub does not support alerting on OS-level dependencies, it will alert on any PyPI dependencies that are defined in the environment.yaml.
1+
# Component detection action
42

3+
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.
54

65
### Example workflow
76

87
```yaml
98

10-
name: Conda dependency submission
9+
name: Component Detection
1110

1211
on:
1312
workflow_dispatch:
@@ -22,6 +21,18 @@ jobs:
2221
runs-on: ubuntu-latest
2322
steps:
2423
- uses: actions/checkout@v3
25-
- name: Conda dependency scanning
26-
uses: jhutchings1/conda-dependency-submission-action@v0.0.2
24+
- name: Component detection
25+
uses: jhutchings1/component-detection-action@v0.0.1
2726
```
27+
28+
### Configuration options
29+
30+
| Parameter | Description | Example |
31+
| --- | --- |
32+
filePath | The path to the directory containing the environment files to upload. Defaults to Actions working directory. | `'.'`
33+
directoryExclusionList | Filters out specific directories following a minimatch pattern. | `test`
34+
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`
35+
dockerImagesToScan |Comma separated list of docker image names or hashes to execute container scanning on | ubuntu:16.04,56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab |
36+
detectorsFilter | A comma separated list with the identifiers of the specific detectors to be used. | `Pip, RustCrateDetector`
37+
38+
For more information: https://github.com/microsoft/component-detection

action.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ inputs:
99
description: 'The path to the directory containing the environment files to upload. Defaults to Actions working directory.'
1010
required: false
1111
default: '.'
12-
filePattern:
13-
description: 'The file name pattern for environment files to upload'
12+
directoryExclusionList:
13+
description: 'Filters out specific directories following a minimatch pattern.'
14+
required: false
15+
detectorArgs:
16+
description: '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.'
17+
required: false
18+
dockerImagesToScan:
19+
description: 'Comma separated list of docker image names or hashes to execute container scanning on, ex: ubuntu:16.04,56bab49eef2ef07505f6a1b0d5bd3a601dfc3c76ad4460f24c91d6fa298369ab'
20+
required: false
21+
detectorsFilter:
22+
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.'
1423
required: false
15-
default: 'environment.yaml'
1624
runs:
1725
using: 'node16'
1826
main: 'dist/index.js'

component-detection

68.7 MB
Binary file not shown.

componentDetection.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import ComponentDetection from './componentDetection';
2+
import fs from 'fs';
3+
4+
test('Downloads CLI', async () => {
5+
ComponentDetection.downloadLatestRelease();
6+
expect(fs.existsSync(ComponentDetection.componentDetectionPath));
7+
});
8+
9+
test('Runs CLI', async () => {
10+
await ComponentDetection.runComponentDetection('./test');
11+
expect(fs.existsSync(ComponentDetection.outputPath));
12+
});
13+
14+
test('Parses CLI output', async () => {
15+
var manifests = await ComponentDetection.getManifestsFromResults();
16+
expect(manifests?.length == 2);
17+
});

componentDetection.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as github from '@actions/github'
2+
import * as core from '@actions/core'
3+
import {
4+
PackageCache,
5+
BuildTarget,
6+
Package,
7+
Snapshot,
8+
Manifest,
9+
submitSnapshot
10+
} from '@github/dependency-submission-toolkit'
11+
import fetch from 'cross-fetch'
12+
import tar from 'tar'
13+
import fs from 'fs'
14+
import * as exec from '@actions/exec';
15+
import dotenv from 'dotenv'
16+
import { Context } from '@actions/github/lib/context'
17+
import { unmockedModulePathPatterns } from './jest.config'
18+
dotenv.config();
19+
20+
export default class ComponentDetection {
21+
public static componentDetectionPath = './component-detection';
22+
public static outputPath = './output.json';
23+
24+
// This is the default entry point for this class.
25+
static async scanAndGetManifests(path: string): Promise<Manifest[] | undefined> {
26+
await this.downloadLatestRelease();
27+
await this.runComponentDetection(path);
28+
return await this.getManifestsFromResults();
29+
}
30+
// Get the latest release from the component-detection repo, download the tarball, and extract it
31+
public static async downloadLatestRelease() {
32+
try {
33+
core.debug("Downloading latest release");
34+
const downloadURL = await this.getLatestReleaseURL();
35+
const blob = await (await fetch(new URL(downloadURL))).blob();
36+
const arrayBuffer = await blob.arrayBuffer();
37+
const buffer = Buffer.from(arrayBuffer);
38+
39+
// Write the blob to a file
40+
core.debug("Writing binary to file");
41+
await fs.writeFileSync(this.componentDetectionPath, buffer, {mode: 0o777, flag: 'w'});
42+
} catch (error: any) {
43+
core.error(error);
44+
}
45+
}
46+
47+
// Run the component-detection CLI on the path specified
48+
public static async runComponentDetection(path: string) {
49+
core.info("Running component-detection");
50+
51+
try {
52+
await exec.exec(`${this.componentDetectionPath} scan --SourceDirectory ${path} --ManifestFile ${this.outputPath} ${this.getComponentDetectionParameters()}`);
53+
} catch (error: any) {
54+
core.error(error);
55+
}
56+
}
57+
58+
private static getComponentDetectionParameters(): string {
59+
var parameters = "";
60+
parameters += (core.getInput('directoryExclusionList')) ? ` --DirectoryExclusionList ${core.getInput('directoryExclusionList')}` : "";
61+
parameters += (core.getInput('detectorArgs')) ? ` --DetectorArgs ${core.getInput('detectorArgs')}` : "";
62+
parameters += (core.getInput('detectorsFilter')) ? ` --DetectorsFilter ${core.getInput('detectorsFilter')}` : "";
63+
parameters += (core.getInput('dockerImagesToScan')) ? ` --DockerImagesToScan ${core.getInput('dockerImagesToScan')}` : "";
64+
return parameters;
65+
}
66+
67+
public static async getManifestsFromResults(): Promise<Manifest[]| undefined> {
68+
core.info("Getting manifests from results");
69+
// Parse the result file and add the packages to the package cache
70+
const packageCache = new PackageCache();
71+
const packages: Array<ComponentDetectionPackage>= [];
72+
73+
const results = await fs.readFileSync(this.outputPath, 'utf8');
74+
75+
var json: any = JSON.parse(results);
76+
json.componentsFound.forEach(async (component: any) => {
77+
const packageUrl = ComponentDetection.makePackageUrl(component.component.packageUrl);
78+
79+
if (!packageCache.hasPackage(packageUrl)) {
80+
const pkg = new ComponentDetectionPackage(packageUrl, component.component.id,
81+
component.isDevelopmentDependency,component.topLevelReferrers,component.locationsFoundAt, component.containerDetailIds, component.containerLayerIds);
82+
packageCache.addPackage(pkg);
83+
packages.push(pkg);
84+
}
85+
});
86+
87+
// Set the transitive dependencies
88+
core.debug("Sorting out transitive dependencies");
89+
packages.forEach(async (pkg: ComponentDetectionPackage) => {
90+
pkg.topLevelReferrers.forEach(async (referrer: any) => {
91+
const referrerPackage = packageCache.lookupPackage(ComponentDetection.makePackageUrl(referrer.packageUrl));
92+
if (referrerPackage) {
93+
referrerPackage.dependsOn(pkg);
94+
}
95+
});
96+
});
97+
98+
// Create manifests
99+
const manifests: Array<Manifest> = [];
100+
101+
// Check the locationsFoundAt for every package and add each as a manifest
102+
packages.forEach(async (pkg: ComponentDetectionPackage) => {
103+
pkg.locationsFoundAt.forEach(async (location: any) => {
104+
if (!manifests.find((manifest: Manifest) => manifest.name == location)) {
105+
const manifest = new Manifest(location, location);
106+
manifests.push(manifest);
107+
}
108+
if (pkg.topLevelReferrers.length == 0) {
109+
manifests.find((manifest: Manifest) => manifest.name == location)?.addDirectDependency(pkg, ComponentDetection.getDependencyScope(pkg));
110+
} else {
111+
manifests.find((manifest: Manifest) => manifest.name == location)?.addIndirectDependency(pkg, ComponentDetection.getDependencyScope(pkg)); }
112+
});
113+
});
114+
return manifests;
115+
}
116+
117+
private static getDependencyScope(pkg: ComponentDetectionPackage) {
118+
return pkg.isDevelopmentDependency ? 'development' : 'runtime'
119+
}
120+
121+
private static makePackageUrl(packageUrlJson: any): string {
122+
var packageUrl = `${packageUrlJson.Scheme}:${packageUrlJson.Type}/`;
123+
if (packageUrlJson.Namespace) {
124+
packageUrl += `${packageUrlJson.Namespace.replaceAll("@", "%40")}/`;
125+
}
126+
packageUrl += `${packageUrlJson.Name.replaceAll("@", "%40")}`;
127+
if (packageUrlJson.Version) {
128+
packageUrl += `@${packageUrlJson.Version}`;
129+
}
130+
if (packageUrlJson.Qualifiers) {
131+
packageUrl += `?${packageUrlJson.Qualifiers}`;
132+
}
133+
return packageUrl;
134+
}
135+
136+
private static async getLatestReleaseURL(): Promise<string> {
137+
const githubToken = core.getInput('token') || process.env.GITHUB_TOKEN2 || "";
138+
const octokit = github.getOctokit(githubToken);
139+
const owner = "microsoft";
140+
const repo = "component-detection";
141+
142+
const latestRelease = await octokit.rest.repos.getLatestRelease({
143+
owner, repo
144+
});
145+
146+
var downloadURL: string = "";
147+
latestRelease.data.assets.forEach((asset: any) => {
148+
if (asset.name === "component-detection-linux-x64") {
149+
downloadURL = asset.browser_download_url;
150+
}
151+
});
152+
153+
return downloadURL;
154+
}
155+
}
156+
157+
class ComponentDetectionPackage extends Package {
158+
159+
constructor(packageUrl: string, public id: string, public isDevelopmentDependency:boolean, public topLevelReferrers: [],
160+
public locationsFoundAt: [], public containerDetailIds: [], public containerLayerIds: []) {
161+
super(packageUrl);
162+
}
163+
}
164+
165+
166+
167+
168+
169+
170+
171+
172+
173+

0 commit comments

Comments
 (0)