src/sbom/sbom.service.ts
Methods |
|
| crossPackageJsonReferenceDeps | |||||||||
crossPackageJsonReferenceDeps(deps: string[], packageJsonDeps: Record
|
|||||||||
|
Defined in src/sbom/sbom.service.ts:181
|
|||||||||
|
cross references the discovered dependencies with the package.json dependencies and devDepeendencies
Parameters :
Returns :
{}
|
| Private deriveGoLikeSBOM | |||||
deriveGoLikeSBOM(undefined: literal type)
|
|||||
|
Defined in src/sbom/sbom.service.ts:129
|
|||||
|
Parameters :
Returns :
string[]
|
| Async deriveJavascriptLikeSBOM | |||||
deriveJavascriptLikeSBOM(undefined: literal type)
|
|||||
|
Defined in src/sbom/sbom.service.ts:45
|
|||||
|
Parameters :
Returns :
Promise<string[]>
|
| Async generateSBOM |
generateSBOM(repoOwner: string, repoName: string)
|
|
Defined in src/sbom/sbom.service.ts:22
|
|
generates an array of SBOM repositories for the provided repository
Returns :
unknown
|
| Async getSBOM |
getSBOM(repoOwner: string, repoName: string)
|
|
Defined in src/sbom/sbom.service.ts:198
|
|
fetches the SBOM information from the GitHub API Credentials are needed for private repos, and to mitigate getting rate limited
Returns :
unknown
|
| parseRepositoryFullName | ||||||
parseRepositoryFullName(url?: string)
|
||||||
|
Defined in src/sbom/sbom.service.ts:116
|
||||||
|
Parameters :
Returns :
any
|
import { Injectable, NotFoundException, PreconditionFailedException } from "@nestjs/common";
interface SBOMResponse {
sbom:
| {
packages: [
{
externalRefs: {
referenceLocator: string;
}[];
}
];
}
| undefined;
}
@Injectable()
export class SBOMService {
/**
* generates an array of SBOM repositories for the provided repository
*/
async generateSBOM(repoOwner: string, repoName: string) {
const sbomData = await this.getSBOM(repoOwner, repoName);
if (!sbomData.sbom) {
throw new NotFoundException();
}
const jsonSBOM = await this.deriveJavascriptLikeSBOM({ repoOwner, repoName, sbomData });
const goSBOM = this.deriveGoLikeSBOM({ sbomData });
/**
* aggregate results from all supported packaging systems.
*/
const results = [...jsonSBOM, ...goSBOM];
if (results.length === 0) {
throw new PreconditionFailedException("The repository provided is not currently supported");
}
return results;
}
async deriveJavascriptLikeSBOM({
sbomData,
repoOwner,
repoName,
}: {
sbomData: SBOMResponse;
repoOwner: string;
repoName: string;
}): Promise<string[]> {
let packageJsonDeps = {};
try {
const packageJsonResponseMeta = await fetch(
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/package.json`
);
if (packageJsonResponseMeta.ok) {
const packageJsonMeta = (await packageJsonResponseMeta.json()) as { download_url: string };
const packageJsonResponse = await fetch(packageJsonMeta.download_url);
if (packageJsonResponse.ok) {
const packageJson = (await packageJsonResponse.json()) as {
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};
packageJsonDeps = { ...packageJson.dependencies };
packageJsonDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
}
}
} catch (e) {
if (e instanceof PreconditionFailedException) {
throw e;
}
console.error(e);
}
const deps: string[] = [];
// collect all the npm packages from the SBM
sbomData.sbom!.packages.forEach((pkg) => {
pkg.externalRefs.forEach((ref) => {
if (ref.referenceLocator.startsWith("pkg:npm")) {
deps.push(ref.referenceLocator.split("pkg:npm/")[1]);
}
});
});
// sanitize the collected dependencies
const discoveredDeps = deps.map((dep) => `${decodeURIComponent(dep).split(/@[0-9]/)[0]}`);
// cross reference direct dependcies with the package.json
const foundDeps = this.crossPackageJsonReferenceDeps(discoveredDeps, packageJsonDeps);
// fetch the npm repository information for each dependency and get the GitHub repo URL
const npmRepos = foundDeps.map(async (dep) => {
const npmPkgResponse = await fetch(`https://registry.npmjs.org/${dep.replace(/\//g, "%2F")}`);
const npmPkg = (await npmPkgResponse.json()) as { repository?: { url: string } };
const repository = this.parseRepositoryFullName(npmPkg.repository?.url);
return repository;
});
const npmResponses = await Promise.all(npmRepos);
// return a unique set of repositories
return Array.from(new Set(npmResponses.filter(Boolean)));
}
parseRepositoryFullName(url?: string) {
const parsedUrl = url
? /* eslint-disable-next-line */ new URL(url, "https://github.com").pathname
.replace("git@github.com:", "")
.split("/")
.slice(1, 3)
.join("/")
.replace(".git", "")
: "";
return parsedUrl;
}
private deriveGoLikeSBOM({ sbomData }: { sbomData: SBOMResponse }): string[] {
const deps: string[] = [];
sbomData.sbom!.packages.forEach((pkg) => {
/**
* go SBOM packages generally come in the form of:
* pkg:golang/github.com/owner/repo-name@1.2.3
*
* - "pkg:golang/" denotes that it's a go package
* - "github.com" denotes the go mod proxy where the module is located
* (i.e., this may be the Google go proxy at golang.org like: golang.org/x/term)
* - "owner/repo-name" is the literal GitHub owner/repo reference. These are
* fully qualified Go modules and are generally referenced as github.com/owner/repo
* in Go code.
* - it's common for deeper import paths to be present
* (for example: github.com/golang-jwt/jwt/v4 where "github.com/golang-jwt/jwt"
* is the top level github dependency). In this case, we take only "github/owner/name"
* - "@1.2.3" is the version of the Go module (i.e., the GitHub release)
*
* We take only the packages that have a well formatted pkg:golang/github.com
* and throw out the versioning
*/
pkg.externalRefs.forEach((ref) => {
if (ref.referenceLocator.startsWith("pkg:golang/github.com")) {
console.log(ref.referenceLocator);
/* eslint-disable-next-line */
const dep = ref.referenceLocator
.split("/")
.slice(2, 4)
.join("/")
.split("@")[0];
deps.push(dep);
}
});
});
/*
* once all Go dependencies are gathered, de-duplicate the array from a unique set.
* because Go deps may have multiple import paths of the same module (i.e., charmbracelt/x/term,
* charmbraclet/x/ansi, charmbraclet/x/windows, etc. etc.), there may be the same top level
* dependency across multiple nested dependencies
*/
return Array.from(new Set(deps));
}
/**
* cross references the discovered dependencies with the package.json dependencies and devDepeendencies
*/
crossPackageJsonReferenceDeps(deps: string[], packageJsonDeps: Record<string, string>) {
// find deps from sbom that are in package.json
const foundDeps: string[] = [];
deps.forEach((dep) => {
if (packageJsonDeps[dep]) {
foundDeps.push(dep);
}
});
return foundDeps;
}
/**
* fetches the SBOM information from the GitHub API
* Credentials are needed for private repos, and to mitigate getting rate limited
*/
async getSBOM(repoOwner: string, repoName: string) {
return fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/dependency-graph/sbom`).then(
(response) => response.json() as unknown as SBOMResponse
);
}
}