130 lines · 5.0 KB
import { all, batch, chunks, MAX_IN_LIST, placeholders, stmt } from "../db";
import { loadRepoById } from "../repos";
import { withStore } from "../git/store";
import { manifestKind } from "./manifests";
import type { Dependency, Ecosystem } from "./manifests";
import { loadSecuritySettings } from "./settings";

const MAX_TREE_ENTRIES = 20_000;
const MAX_MANIFESTS = 50;
const MAX_MANIFEST_BYTES = 2 * 1024 * 1024;
const MAX_DEPENDENCIES = 5000;
const DEPENDENCY_COLUMNS = 7;
const STATEMENTS_PER_BATCH = 50;
const DEPENDENCY_PAGE = 100;
const MAX_NAME_LENGTH = 214;
const decoder = new TextDecoder();

type Parsed = { path: string; ecosystem: Ecosystem; dependencies: Dependency[] };

/** Rebuilds the dependency graph for a commit from its manifests and lockfiles, within fixed budgets. */
export async function scanDependencies(repoId: string, sha: string): Promise<number> {
  const repo = await loadRepoById(repoId);
  if (!repo || !(await loadSecuritySettings(repo.id)).dependencyGraph) return 0;
  const parsed = await withStore(repo.artifacts_name, async (store) => {
    const commit = await store.commit(sha);
    if (!commit) return null;
    const { entries } = await store.walk(commit.treeSha, MAX_TREE_ENTRIES);
    const manifests = entries
      .filter((entry) => entry.type === "blob" && manifestKind(entry.path))
      .slice(0, MAX_MANIFESTS);
    const out: Parsed[] = [];
    let total = 0;
    for (const entry of manifests) {
      const kind = manifestKind(entry.path);
      const blob = kind ? await store.blob(entry.sha) : null;
      if (!kind || !blob || blob.byteLength > MAX_MANIFEST_BYTES) continue;
      const unique = new Map<string, Dependency>();
      for (const dependency of kind.parse(decoder.decode(blob))) {
        const key = `${dependency.name}\0${dependency.scope}`;
        if (!unique.has(key) && dependency.name.length <= MAX_NAME_LENGTH) unique.set(key, dependency);