File

src/sbom/sbom.service.ts

Index

Methods

Methods

crossPackageJsonReferenceDeps
crossPackageJsonReferenceDeps(deps: string[], packageJsonDeps: Record)

cross references the discovered dependencies with the package.json dependencies and devDepeendencies

Parameters :
Name Type Optional
deps string[] No
packageJsonDeps Record<string | string> No
Returns : {}
Private deriveGoLikeSBOM
deriveGoLikeSBOM(undefined: literal type)
Parameters :
Name Type Optional
literal type No
Returns : string[]
Async deriveJavascriptLikeSBOM
deriveJavascriptLikeSBOM(undefined: literal type)
Parameters :
Name Type Optional
literal type No
Returns : Promise<string[]>
Async generateSBOM
generateSBOM(repoOwner: string, repoName: string)

generates an array of SBOM repositories for the provided repository

Parameters :
Name Type Optional
repoOwner string No
repoName string No
Returns : unknown
Async getSBOM
getSBOM(repoOwner: string, repoName: string)

fetches the SBOM information from the GitHub API Credentials are needed for private repos, and to mitigate getting rate limited

Parameters :
Name Type Optional
repoOwner string No
repoName string No
Returns : unknown
parseRepositoryFullName
parseRepositoryFullName(url?: string)
Parameters :
Name Type Optional
url string Yes
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
    );
  }
}

results matching ""

    No results matching ""