// sbom/generate_sbom.jsx — "Generate from source code", the second way to get an
// SBOM beside uploading one. The user drops a .zip of the source tree — or one firmware
// image or compiled file — and chooses nothing else: the server (POST /api/sbom/generate,
// no "tool") runs every generator it has in the background — its own extractor, the C/C++
// source check and linker maps, the binary analysis of compiled files and ECU images, and
// Syft and cdxgen where deployed — and merges what they found into one CycloneDX
// SBOM, each component once. It answers with that SBOM and its parse by the SAME server
// parser an uploaded file goes through, so from then on the wizard and the update
// dialog cannot tell the two apart: the matching, the review and the saved component
// are identical.
//
// Firmware and compiled files only for a company granted binary analysis
// (FEATURES.binaryAnalysis). Without it the drop zone asks for a source .zip alone, and
// the server refuses anything else, and lists firmware and native binaries in a .zip
// without analysing them, which it says in the SBOM's warnings.
//
// Used by the wizard's SBOM step (wizard_components.jsx) and the update-SBOM dialog
// (update_sbom.jsx).

// What the binary analysis read, as the tooltip of its count in the wizard's header: the
// formats and CPUs, the compilers the files name, firmware images and their headers, the
// compiled files it recognised nothing in (worth a look by hand) and the files it did not read.
function sbomBinaryTitle(binary) {
  if (!binary) return "";
  const lines = [];
  const formats = Object.entries(binary.formats || {}).map(([f, n]) => `${f} ${n}`).join(", ");
  if (formats) lines.push("Read: " + formats);
  if ((binary.architectures || []).length) lines.push("CPUs: " + binary.architectures.join(", "));
  if ((binary.compilers || []).length) lines.push("Compilers: " + binary.compilers.slice(0, 4).join(" | "));
  for (const f of (binary.firmware || []).slice(0, 5)) {
    const header = Object.entries(f.header || {}).map(([k, v]) => `${k} ${v}`).join(", ");
    lines.push(`Image ${f.path}: ${f.format}, ${f.regions} region(s) ${f.lowestAddress || ""}–${f.highestAddress || ""}` + (header ? ` (${header})` : "")
      + (f.decoding ? `; ${f.decoding}` : ""));
  }
  if ((binary.unpacked || []).length) {
    lines.push("Unpacked and read inside:");
    for (const n of binary.unpacked.slice(0, 10)) lines.push("  " + n.path + ": " + n.note);
  }
  if ((binary.unidentified || []).length) {
    lines.push("Nothing recognised in (look at these by hand):");
    for (const n of binary.unidentified.slice(0, 10)) lines.push("  " + n.path + " (" + n.note + ")");
  }
  if ((binary.skipped || []).length) {
    lines.push("Not read, or only in part:");
    for (const n of binary.skipped.slice(0, 10)) lines.push("  " + n.path + ": " + n.note);
  }
  if (binary.stoppedEarly) lines.push("The time for the scan ran out before every file was read.");
  return lines.join("\n");
}

// "IAR Embedded Workbench for Arm 9.30.1.335": a toolchain as the header names it.
function sbomToolchainLabel(t) {
  return t.name + (t.version ? " " + t.version : "");
}

// The toolchains that built the firmware and what says so, as the tooltip of their names.
function sbomToolchainTitle(toolchains) {
  const lines = [];
  for (const t of toolchains || []) {
    lines.push(sbomToolchainLabel(t) + (t.supplier ? " (" + t.supplier + ")" : ""));
    for (const e of (t.evidence || []).slice(0, 4)) lines.push("  " + e.source + (e.path ? " " + e.path : "") + ": " + e.detail);
  }
  return lines.join("\n");
}

// What a compile_commands.json said: which libraries the build compiled.
function sbomCompileTitle(cc) {
  if (!cc) return "";
  return [
    "Read: " + (cc.files || []).join(", "),
    cc.entries + " files compiled, " + cc.tracedFiles + " of them found in the upload",
    (cc.compilers || []).length ? "Compilers: " + cc.compilers.join(", ") : "",
    cc.unknown ? cc.unknown + " libraries it cannot speak for (header-only, or built elsewhere)" : "",
  ].filter(Boolean).join("\n");
}

// Every file of the upload with its SHA-256, as CSV: the record of exactly what was uploaded.
function sbomInventoryCsv(inventory) {
  const cell = (v) => {
    const s = String(v == null ? "" : v);
    return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  const rows = ["path,kind,size_bytes,sha256"];
  for (const e of (inventory && inventory.entries) || []) rows.push([e.path, e.kind, e.size, e.sha256 || ""].map(cell).join(","));
  if (inventory && inventory.truncated)
    rows.push(cell(`(the first ${inventory.entries.length} of ${inventory.files} files are listed)`) + ",,,");
  return rows.join("\r\n") + "\r\n";
}

// Every component an SBOM lists goes into the inventory and is matched: nothing is set
// aside, including what the SBOM marks excluded (only used to develop, test or build, or
// not linked into the image), and that mark is dropped. Each package is listed once: the
// server already merges exact repeats, and here entries with the same name and version
// (two tools that spelled the purl or type differently) become one too, keeping what each
// adds, with dependency edges moved to the entry kept. Shared by the wizard and the update
// dialog. Returns { libs, relationships, merged }.
function sbomAllLibs(parsedLibs, relationships) {
  const norm = (s) => String(s == null ? "" : s).trim().toLowerCase();
  const version = (v) => norm(v).replace(/^v(?=\d)/, "");
  const byKey = new Map();
  const aliases = new Map();       // bom-ref of a merged entry -> bom-ref of the one kept
  const libs = [];
  let merged = 0;
  for (const { scope, onlyFor, ...lib } of parsedLibs || []) {
    const key = norm(lib.name) + "@" + version(lib.version);
    const kept = byKey.get(key);
    if (!kept) {
      const copy = { ...lib };
      byKey.set(key, copy);
      libs.push(copy);
      continue;
    }
    merged++;
    for (const f of ["purl", "cpe", "supplier", "partNumber", "evidence", "bomRef"])
      if ((kept[f] == null || kept[f] === "" || kept[f] === "—") && lib[f]) kept[f] = lib[f];
    for (const f of ["licenses", "foundBy"])
      if (Array.isArray(lib[f])) kept[f] = Array.from(new Set([...(kept[f] || []), ...lib[f]]));
    if (Array.isArray(lib.hashes))
      kept.hashes = [...(kept.hashes || []), ...lib.hashes.filter(h => !(kept.hashes || []).some(k => norm(k.alg) === norm(h.alg)))];
    if (lib.bomRef && kept.bomRef && lib.bomRef !== kept.bomRef) aliases.set(lib.bomRef, kept.bomRef);
  }
  const seen = new Set();
  const edges = [];
  for (const r of relationships || []) {
    const from = aliases.get(r.from) || r.from, to = aliases.get(r.to) || r.to;
    const k = from + "\u0001" + to + "\u0001" + (r.type || "");
    if (from === to || seen.has(k)) continue;
    seen.add(k);
    edges.push({ ...r, from, to });
  }
  return { libs, relationships: edges, merged };
}

// "820 MB", "4.3 GB", "5 GB".
function sbomgenSize(bytes) {
  const gb = 1024 * 1024 * 1024;
  const n = bytes >= gb ? bytes / gb : bytes / (1024 * 1024);
  const shown = n >= 10 ? Math.round(n) : Math.round(n * 10) / 10;
  return shown + (bytes >= gb ? " GB" : " MB");
}

// What to tell the user when a generation fails: the server says what was the matter
// with the upload (a few-word title and one or two sentences), and api.js says what a
// missing or unreadable answer means. Shown as given.
function sbomgenError(e) {
  return { title: (e && e.title) || "", detail: (e && e.message) || "The SBOM could not be generated.", reason: (e && e.reason) || "" };
}

// The red box under the drop zone: the cause in bold, then what to do.
function SbomgenErr({ err, style }) {
  if (!err) return null;
  const { title, detail } = typeof err === "string" ? { title: "", detail: err } : err;
  return (
    <div className="login-err gen-err" role="alert" style={style}>
      <Icon name="bell" size={14} />
      <span>{title && <b className="gen-err-title">{title}</b>}{detail}</span>
    </div>
  );
}

// What a generated SBOM may be missing, and why (a scanner that did not finish, a file that
// could not be read). Above the inventory, not in a tooltip: a list of components reads as the
// whole truth about a product, so a partial one has to say so before anyone relies on it.
function SbomgenWarnings({ warnings }) {
  if (!warnings || !warnings.length) return null;
  return (
    <div className="gen-warn" role="status">
      <Icon name="alert" size={14} />
      <div>
        <b className="gen-warn-head">This SBOM may be incomplete</b>
        <ul>{warnings.map((w) => <li key={w.reason + w.detail}>{w.detail}</li>)}</ul>
      </div>
    </div>
  );
}

// What was dropped, when it cannot go up as one file: a folder (the browser lists it as a file
// that fails only once the upload starts, as a lost connection) or several files (only the first
// would be sent, and nothing would say so). `one` says what to do instead; null when it is fine.
function sbomDropProblem(dataTransfer, one) {
  const items = Array.from((dataTransfer && dataTransfer.items) || []).filter((i) => i.kind === "file");
  const entry = items.length === 1 && items[0].webkitGetAsEntry ? items[0].webkitGetAsEntry() : null;
  if (entry && entry.isDirectory)
    return { title: "That is a folder", detail: `${entry.name} is a folder. ${one.folder}` };
  const count = (dataTransfer && dataTransfer.files && dataTransfer.files.length) || 0;
  return count > 1 ? { title: "Several files", detail: `${count} files were dropped. ${one.several}` } : null;
}

const SBOM_DROP_SOURCE = { folder: "Zip it and upload the .zip.", several: "Zip them and upload the .zip." };
const SBOM_DROP_FILE = { folder: "Upload the SBOM file inside it.", several: "Upload one SBOM file at a time." };

// Saves text as a file: a generated SBOM (the server keeps nothing it generates, so
// until the component is created this is the user's only copy) or the CSV template.
function sbomDownload(fileName, text) {
  const type = /\.csv$/i.test(fileName || "") ? "text/csv"
    : /\.xml$/i.test(fileName || "") ? "application/xml" : "application/json";
  sbomSaveBlob(fileName || "sbom.cdx.json", new Blob([text || ""], { type }));
}

function sbomSaveBlob(fileName, blob) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// The formats a generated SBOM can be saved in: the CycloneDX file exactly as it was
// generated, and SPDX and CSV written from it by the server (POST /api/sbom/convert);
// with the scan's file inventory, that too. Each option resolves { blob, fileName }.
function generatedSbomOptions(fileName, raw, specVersion, inventory) {
  const stem = String(fileName || "sbom").replace(/\.cdx\.json$|\.json$/i, "");
  const files = inventory && (inventory.entries || []).length
    ? [{ id: "inventory", label: "File inventory (CSV)", hint: `Every file in the upload (${inventory.files}), with its SHA-256`,
         get: async () => ({ fileName: stem + ".files.csv", blob: new Blob([sbomInventoryCsv(inventory)], { type: "text/csv" }) }) }]
    : [];
  return [
    { id: "cyclonedx", label: "CycloneDX" + (specVersion ? " " + specVersion : "") + " (JSON)",
      hint: "The SBOM exactly as it was generated",
      get: async () => ({ fileName: fileName || "sbom.cdx.json", blob: new Blob([raw || ""], { type: "application/json" }) }) },
    { id: "spdx", label: "SPDX 2.3 (JSON)", hint: "For tools and customers that ask for SPDX",
      get: () => window.VCApi.convertSbom(raw, fileName, "spdx") },
    { id: "csv", label: "CSV component list", hint: "Opens in a spreadsheet, and can be uploaded again",
      get: () => window.VCApi.convertSbom(raw, fileName, "csv") },
    ...files,
  ];
}

// "Download SBOM ▾": one button, the formats in a menu. `options` are
// [{ id, label, hint, get: () => Promise<{ blob, fileName }> }]. The menu owns the busy
// state and the error, so every screen that offers SBOM downloads behaves the same.
// Escape or a click elsewhere closes it; the arrow keys move between formats.
function SbomDownloadMenu({ options, label = "Download SBOM", block }) {
  const [open, setOpen] = useState(false);
  const [busy, setBusy] = useState("");          // the option being prepared
  const [err, setErr] = useState("");
  const wrapRef = useRef(null);
  const btnRef = useRef(null);
  const listRef = useRef(null);

  useEffect(() => {
    if (!open) return;
    function away(e) { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }
    function key(e) {
      if (e.key !== "Escape") return;
      setOpen(false);
      if (btnRef.current) btnRef.current.focus();
    }
    document.addEventListener("mousedown", away);
    document.addEventListener("keydown", key);
    const first = listRef.current && listRef.current.querySelector("[role=menuitem]");
    if (first) first.focus();
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", key); };
  }, [open]);

  function onListKey(e) {
    if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
    e.preventDefault();
    const items = Array.from(listRef.current.querySelectorAll("[role=menuitem]"));
    const at = items.indexOf(document.activeElement);
    const next = items[(at + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length];
    if (next) next.focus();
  }

  async function pick(option) {
    setOpen(false); setErr(""); setBusy(option.id);
    try {
      const file = await option.get();
      sbomSaveBlob(file.fileName, file.blob);
    } catch (e) {
      if (e && e.status === 401 && window.__vcOn401) { window.__vcOn401(); return; }
      setErr((e && e.message) || "Could not prepare that file.");
    } finally {
      setBusy("");
      if (btnRef.current) btnRef.current.focus();
    }
  }

  return (
    <div className={"dlmenu" + (block ? " dlmenu-block" : "")} ref={wrapRef}>
      <button ref={btnRef} type="button" className={"btn btn-sm" + (block ? " btn-block" : "")}
        aria-haspopup="menu" aria-expanded={open} disabled={!!busy} onClick={() => setOpen(o => !o)}>
        <Icon name="download" size={14} />{busy ? "Preparing…" : label}
        <Icon name="chevD" size={13} className="dlmenu-caret" />
      </button>
      {open && (
        <div className="dlmenu-list" role="menu" aria-label={label} ref={listRef} onKeyDown={onListKey}>
          {options.map(o => (
            <button key={o.id} type="button" role="menuitem" className="dlmenu-item" onClick={() => pick(o)}>
              <span className="dlmenu-lab">{o.label}</span>
              {o.hint && <span className="dlmenu-hint">{o.hint}</span>}
            </button>
          ))}
        </div>
      )}
      {err && <div className="login-err dlmenu-err" role="alert"><Icon name="bell" size={13} />{err}</div>}
    </div>
  );
}

// componentName labels the SBOM's primary component (the wizard passes what the user
// typed in the Identity step). onGenerated receives what an uploaded file reads as
// ({ meta, libs, relationships, raw, filename }) plus
// generated: { tool, toolName, source, componentCount, durationMs, … }.
function SourceSbomGenerator({ componentName, onGenerated, compact }) {
  const [enabled, setEnabled] = useState(null);      // can this server generate at all; null while asking
  const [enabledErr, setEnabledErr] = useState("");
  const [checks, setChecks] = useState(0);           // bumped by "Try again" after that question failed
  const [phase, setPhase] = useState("idle");        // idle | uploading | scanning
  const [progress, setProgress] = useState(0);
  const [uploadSize, setUploadSize] = useState(0);   // bytes of the archive being sent
  const [err, setErr] = useState("");
  const [drag, setDrag] = useState(false);
  const inputRef = useRef(null);
  // Leaving the screen mid-scan cancels the request, and with it the scan on the
  // server — otherwise it would keep the server's single scan slot busy for nobody.
  const ctrlRef = useRef(null);
  useEffect(() => () => { if (ctrlRef.current) ctrlRef.current.abort(); }, []);

  useEffect(() => {
    const ctrl = new AbortController();
    setEnabled(null); setEnabledErr("");
    // Which generators run is the server's business; the page only needs to know that
    // generation is switched on (the built-in extractor is always there when it is).
    window.VCApi.sbomGenerators(ctrl.signal).then(
      (list) => setEnabled((Array.isArray(list) ? list : []).some((t) => t.available)),
      (e) => {
        if (e && e.aborted) return;
        if (e && e.status === 401 && window.__vcOn401) { window.__vcOn401(); return; }
        setEnabled(false);
        setEnabledErr((e && e.message) || "Could not reach the server. Try again.");
      });
    return () => ctrl.abort();
  }, [checks]);

  const anyAvailable = enabled === true;
  const maxBytes = window.VCApi.SBOM_SOURCE_MAX_BYTES;
  const binaryOn = FEATURES.binaryAnalysis;

  // The grant can arrive under an open panel, and its refusal must not stay drawn beneath
  // the offer it now contradicts.
  useEffect(() => { setErr((e) => (e && e.reason === "binary-analysis-off" ? "" : e)); }, [binaryOn]);

  async function generate(file) {
    if (!file || phase !== "idle") return;
    setErr("");
    // Any file: a .zip is unpacked, anything else is read as one compiled file (the server
    // decides by its content, not its name).
    if (file.size > maxBytes) {
      setErr({ title: "File too large", detail: `${file.name} is ${sbomgenSize(file.size)}. The limit is ${sbomgenSize(maxBytes)}.` });
      return;
    }
    if (!anyAvailable) return;

    const ctrl = new AbortController();
    ctrlRef.current = ctrl;
    setPhase("uploading"); setProgress(0); setUploadSize(file.size);
    try {
      const result = await window.VCApi.generateSbom(file, null, {
        name: componentName || "",
        signal: ctrl.signal,
        onProgress: (p) => {
          if (ctrl.signal.aborted) return;
          setProgress(p);
          if (p >= 100) setPhase("scanning");
        },
      });
      if (ctrl.signal.aborted) return;
      if (!result || !result.sbom || !result.parsed)
        throw Object.assign(new Error("The server answered without an SBOM. Try again."), { title: "No SBOM in the answer" });
      const parsed = result.parsed;
      if (!Array.isArray(parsed.libs) || !parsed.libs.length)
        throw Object.assign(new Error("The generated SBOM has no components. Try again."), { title: "Empty SBOM" });
      setPhase("idle");
      onGenerated({
        meta: parsed.meta,
        libs: parsed.libs,
        relationships: parsed.relationships || [],
        repeatsMerged: parsed.repeatsMerged || 0,
        raw: String(result.sbom),
        filename: result.fileName,
        generated: {
          tool: result.tool,
          toolName: "VulnerabilityCore",
          source: file.name,
          componentCount: result.componentCount,
          // How many of those the C/C++ source check added (vendored libraries by their
          // version headers, AUTOSAR modules).
          sourceDetectedCount: result.sourceDetectedCount || 0,
          // Packages read from lock files / manifests, and the files read.
          packageCount: result.packageCount || 0,
          manifests: Array.isArray(result.manifests) ? result.manifests : [],
          // What the background scanners found that nothing else had (binaries, more ecosystems).
          scannerAddedCount: result.scannerAddedCount || 0,
          // What a linker map in the archive said (null when there was none).
          linkerMap: result.linkerMap || null,
          // What the binary analysis found in compiled files and ECU images, and what it read
          // (formats, architectures, files it recognised nothing in; null without any).
          binaryDetectedCount: result.binaryDetectedCount || 0,
          binary: result.binary || null,
          // What a compile_commands.json said (null without one), the toolchains that built the
          // firmware, every file of the upload with its SHA-256, and how many components got a licence.
          compileCommands: result.compileCommands || null,
          toolchains: Array.isArray(result.toolchains) ? result.toolchains : [],
          inventory: result.inventory || null,
          licensedCount: result.licensedCount || 0,
          // What this SBOM may be missing (see SbomgenWarnings).
          warnings: Array.isArray(result.warnings) ? result.warnings : [],
          durationMs: result.durationMs,
        },
      });
    } catch (e) {
      if (ctrl.signal.aborted || (e && e.aborted)) return;   // left the screen: nobody to tell
      setPhase("idle");
      if (e && e.status === 401 && window.__vcOn401) { window.__vcOn401(); return; }
      setErr(sbomgenError(e));
    } finally {
      if (ctrlRef.current === ctrl) ctrlRef.current = null;
    }
  }

  if (phase !== "idle") {
    const uploading = phase === "uploading";
    return (
      <div className={"drop is-parsing" + (compact ? " drop-compact" : "")} aria-live="polite">
        <span className="btn-spin drop-spin" />
        <strong>{uploading ? `Uploading… ${progress}%` : "Generating the SBOM…"}</strong>
        <span className="drop-sub">{uploading
          ? (uploadSize ? `${sbomgenSize(uploadSize)} · ` : "") + "it is deleted from the server as soon as the SBOM is made"
          : `Unpacking · reading lock files, manifests, packages and C/C++ sources · ${binaryOn ? "analysing compiled files and firmware images · " : ""}merging what was found · a large upload can take a few minutes`}</span>
        {uploading && <span className="gen-progress"><span style={{ width: progress + "%" }} /></span>}
      </div>
    );
  }

  return (
    <div className="gen">
      <input ref={inputRef} type="file" accept={binaryOn ? window.VCApi.SBOM_SOURCE_ACCEPT : window.VCApi.SBOM_SOURCE_ONLY_ACCEPT} style={{ display: "none" }}
        onChange={(e) => { const f = e.target.files && e.target.files[0]; e.target.value = ""; generate(f); }} />
      <div className={"drop" + (drag ? " is-drag" : "") + (anyAvailable ? "" : " is-off") + (compact ? " drop-compact" : "")}
        onDragOver={(e) => { e.preventDefault(); if (anyAvailable) setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={(e) => {
          e.preventDefault(); setDrag(false);
          if (!anyAvailable) return;
          const problem = sbomDropProblem(e.dataTransfer, SBOM_DROP_SOURCE);
          if (problem) setErr(problem);
          else generate(e.dataTransfer.files && e.dataTransfer.files[0]);
        }}
        onClick={() => { if (anyAvailable && inputRef.current) inputRef.current.click(); }}
        style={{ cursor: anyAvailable ? "pointer" : "default" }}>
        <span className="drop-ic"><Icon name="package" size={26} /></span>
        <strong>{enabled === null
          ? "Checking that this server can generate SBOMs…"
          : anyAvailable
            ? (binaryOn
              ? "Drag a .zip of the source code, or a firmware image or binary, here — or click to browse"
              : "Drag a .zip of the source code here — or click to browse")
            : enabledErr
              ? "Could not check whether this server can generate SBOMs"
              : "Generating SBOMs from source code is switched off on this server"}</strong>
        <span className="drop-sub">{enabled === null ? " " : anyAvailable
          ? <>{binaryOn
              ? <>Source and build output as a <b>.zip</b>, or one compiled file (<b>.hex .s19 .vbf .bin .elf .dll</b>…)</>
              : <>Source code as a <b>.zip</b></>} · up to <b>{sbomgenSize(maxBytes)}</b> · deleted from the server as soon as the SBOM is made</>
          : (enabledErr || "Upload an SBOM file instead.")}</span>
        {enabledErr && (
          <div className="drop-actions">
            <button className="btn" onClick={(e) => { e.stopPropagation(); setChecks((n) => n + 1); }}>
              <Icon name="refresh" size={14} />Try again</button>
          </div>
        )}
        {anyAvailable && (
          <div className="drop-actions">
            <button className="btn btn-accent" onClick={(e) => { e.stopPropagation(); if (inputRef.current) inputRef.current.click(); }}>
              <Icon name="upload" size={14} />Choose file</button>
          </div>
        )}
      </div>

      {/* <p className="gen-note">
        <Icon name="alert" size={13} />
        <span>Everything is read in one pass on the server, and each component is listed once however many ways it was found: lock files, manifests and package databases (npm, PyPI, Maven and Gradle, NuGet, Go, Cargo, Conan, vcpkg, CMake, Zephyr west, ESP-IDF, PlatformIO, Keil and CubeMX projects, Linux root filesystems and more), and the C/C++ sources. Lock files give exact versions (<span className="mono">package-lock.json</span>, <span className="mono">poetry.lock</span>, <span className="mono">Cargo.lock</span>, <span className="mono">conan.lock</span>…); a manifest alone gives a version only where it pins one. For C/C++ code the source itself is checked too: libraries copied into the tree by the version their own header states (FreeRTOS, lwIP, Mbed TLS, wolfSSL, zlib, OpenSSL and more), AUTOSAR modules by their vendor and version macros, Infineon iLLD, and git submodules. Put the build's linker <span className="mono">.map</span> file (GNU/HighTec or TASKING) in the zip too: it shows which libraries the firmware really links. Code that states no version anywhere is still not found, so check the inventory afterwards and add what is missing.</span>
      </p> */}

      <SbomgenErr err={err} />
    </div>
  );
}
