Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update license Component for Github SBOM #9755

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ src/pluginMap.ts
/apps_backup/*

# Federation Temp files
/.__mf__temp
/.__mf__temp
src/components/Licenses/beBomData.json
Comment on lines +69 to +70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's keep this inside the public dir. instead.

(we'd also be able to access it by care.ohc.network/licenses/backend|frontend.json)

src/components/Licenses/feBomData.json
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"supported-browsers": "node ./scripts/generate-supported-browsers.mjs",
"build": "npm run setup && npm run build:meta && npm run supported-browsers && npm run build:react",
"setup": "tsx scripts/setup-care-apps.ts",
"postinstall": "tsx scripts/install-platform-deps.ts",
"postinstall": "tsx scripts/install-platform-deps.ts && node scripts/fetchSbomData.js",

"test": "snyk test",
"cypress:open": "cross-env NODE_ENV=development cypress open",
"cypress:run": "cross-env NODE_ENV=development cypress run",
Expand Down
49 changes: 49 additions & 0 deletions scripts/fetchSbomData.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's keep the script in typescript similar to the other postinstall script

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from "fs";
import fetch from "node-fetch";

const fetchSBOMData = async (url) => {
const response = await fetch(url, {
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
throw new Error(`Error fetching SBOM data from ${url}`);
}
return await response.json();
};
Comment on lines +1 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ESM compatibility and add GitHub authentication.

The code has several issues that need to be addressed:

  1. ES modules syntax requires "type": "module" in package.json
  2. GitHub API calls should use authentication to avoid rate limiting
  3. API version should be configurable for future compatibility

Apply these changes:

+// Add to package.json
+{
+  "type": "module"
+}

 import fs from "fs";
 import fetch from "node-fetch";
+import dotenv from "dotenv";
+
+dotenv.config();
+
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
 
 const fetchSBOMData = async (url) => {
   const response = await fetch(url, {
     headers: {
       Accept: "application/vnd.github+json",
+      Authorization: `Bearer ${GITHUB_TOKEN}`,
       "X-GitHub-Api-Version": "2022-11-28",
     },
   });

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Lint Code Base

[error] 1-1: Cannot use import statement outside a module. Need to set "type": "module" in package.json or use .mjs extension


const fetchData = async () => {
const feUrl =
"https://api.github.com/repos/ohcnetwork/care_fe/dependency-graph/sbom";
const beUrl =
"https://api.github.com/repos/ohcnetwork/care/dependency-graph/sbom";

try {
const [frontendData, backendData] = await Promise.all([
fetchSBOMData(feUrl),
fetchSBOMData(beUrl),
]);

// Write frontend SBOM data
fs.writeFileSync(
"./src/components/Licenses/feBomData.json",
JSON.stringify(frontendData, null, 2),
);

// Write backend SBOM data
fs.writeFileSync(
"./src/components/Licenses/beBomData.json",
JSON.stringify(backendData, null, 2),
);

console.log(
"SBOM data successfully saved as feBomData.json and beBomData.json",
);
} catch (error) {
console.error("Error fetching SBOM data:", error.message);
}
};
Comment on lines +17 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and configuration management.

The function needs several improvements:

  1. Repository URLs should be configurable
  2. File operations need proper error handling
  3. SBOM data structure should be validated

Apply these changes:

+const CONFIG = {
+  repositories: {
+    frontend: "ohcnetwork/care_fe",
+    backend: "ohcnetwork/care"
+  },
+  outputPath: "./src/components/Licenses"
+};
+
+const validateSBOMData = (data) => {
+  if (!data?.sbom?.packages) {
+    throw new Error("Invalid SBOM data structure");
+  }
+};
+
 const fetchData = async () => {
-  const feUrl =
-    "https://api.github.com/repos/ohcnetwork/care_fe/dependency-graph/sbom";
-  const beUrl =
-    "https://api.github.com/repos/ohcnetwork/care/dependency-graph/sbom";
+  const feUrl = `https://api.github.com/repos/${CONFIG.repositories.frontend}/dependency-graph/sbom`;
+  const beUrl = `https://api.github.com/repos/${CONFIG.repositories.backend}/dependency-graph/sbom`;

   try {
     const [frontendData, backendData] = await Promise.all([
       fetchSBOMData(feUrl),
       fetchSBOMData(beUrl),
     ]);

+    validateSBOMData(frontendData);
+    validateSBOMData(backendData);
+
+    fs.mkdirSync(CONFIG.outputPath, { recursive: true });
+
     // Write frontend SBOM data
     fs.writeFileSync(
-      "./src/components/Licenses/feBomData.json",
+      `${CONFIG.outputPath}/feBomData.json`,
       JSON.stringify(frontendData, null, 2),
     );

     // Write backend SBOM data
     fs.writeFileSync(
-      "./src/components/Licenses/beBomData.json",
+      `${CONFIG.outputPath}/beBomData.json`,
       JSON.stringify(backendData, null, 2),
     );

     console.log(
       "SBOM data successfully saved as feBomData.json and beBomData.json",
     );
   } catch (error) {
     console.error("Error fetching SBOM data:", error.message);
+    process.exit(1);
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchData = async () => {
const feUrl =
"https://api.github.com/repos/ohcnetwork/care_fe/dependency-graph/sbom";
const beUrl =
"https://api.github.com/repos/ohcnetwork/care/dependency-graph/sbom";
try {
const [frontendData, backendData] = await Promise.all([
fetchSBOMData(feUrl),
fetchSBOMData(beUrl),
]);
// Write frontend SBOM data
fs.writeFileSync(
"./src/components/Licenses/feBomData.json",
JSON.stringify(frontendData, null, 2),
);
// Write backend SBOM data
fs.writeFileSync(
"./src/components/Licenses/beBomData.json",
JSON.stringify(backendData, null, 2),
);
console.log(
"SBOM data successfully saved as feBomData.json and beBomData.json",
);
} catch (error) {
console.error("Error fetching SBOM data:", error.message);
}
};
const CONFIG = {
repositories: {
frontend: "ohcnetwork/care_fe",
backend: "ohcnetwork/care"
},
outputPath: "./src/components/Licenses"
};
const validateSBOMData = (data) => {
if (!data?.sbom?.packages) {
throw new Error("Invalid SBOM data structure");
}
};
const fetchData = async () => {
const feUrl = `https://api.github.com/repos/${CONFIG.repositories.frontend}/dependency-graph/sbom`;
const beUrl = `https://api.github.com/repos/${CONFIG.repositories.backend}/dependency-graph/sbom`;
try {
const [frontendData, backendData] = await Promise.all([
fetchSBOMData(feUrl),
fetchSBOMData(beUrl),
]);
validateSBOMData(frontendData);
validateSBOMData(backendData);
fs.mkdirSync(CONFIG.outputPath, { recursive: true });
// Write frontend SBOM data
fs.writeFileSync(
`${CONFIG.outputPath}/feBomData.json`,
JSON.stringify(frontendData, null, 2),
);
// Write backend SBOM data
fs.writeFileSync(
`${CONFIG.outputPath}/beBomData.json`,
JSON.stringify(backendData, null, 2),
);
console.log(
"SBOM data successfully saved as feBomData.json and beBomData.json",
);
} catch (error) {
console.error("Error fetching SBOM data:", error.message);
process.exit(1);
}
};


fetchData();
127 changes: 42 additions & 85 deletions src/components/Licenses/SBOMViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,35 @@
import dayjs from "dayjs";
import React, { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { useTranslation } from "react-i18next";

import Card from "@/CAREUI/display/Card";
import CareIcon from "@/CAREUI/icons/CareIcon";

import beBomData from "@/components/Licenses/be-sbom.json";
import feBomData from "@/components/Licenses/fe-sbom.json";
import licenseUrls from "@/components/Licenses/licenseUrls.json";

import beBomData from "./beBomData.json";
import feBomData from "./feBomData.json";

const getLicenseUrl = (licenseId: string | undefined): string | null => {
if (!licenseId) return null;
return licenseUrls[licenseId as keyof typeof licenseUrls] || null;
};
interface CycloneDXExternalRef {
url?: string;
type?: string;
comment?: string;
}

interface CycloneDXLicense {
license?: {
id?: string;
};
}

interface CycloneDXProperties {
name?: string;
value?: string;
}

interface CycloneDXComponent {
type?: string;
name?: string;
group?: string;
version?: string;
bomRef?: string;
author?: string;
description?: string;
licenses?: CycloneDXLicense[];
externalReferences?: CycloneDXExternalRef[];
properties?: CycloneDXProperties[];
}

interface CycloneDXTool {
name?: string;
version?: string;
vendor?: string;
externalReferences?: CycloneDXExternalRef[];
}

interface CycloneDXBOM {
bomFormat?: string;
specVersion?: string;
version?: number;
serialNumber?: string;
metadata?: {
timestamp?: string;
tools?: CycloneDXTool[];
component?: CycloneDXComponent;
};
components?: CycloneDXComponent[];
}

const BOMDisplay: React.FC = () => {
const { t } = useTranslation();
const [copyStatus, setCopyStatus] = useState(false);
const [showExternalRefs, setShowExternalRefs] = useState<number | null>(null);
const [activeTab, setActiveTab] = useState<string>("bom");

const bomData = activeTab === "bom" ? feBomData : beBomData;

const handleCopy = () => {
setCopyStatus(true);
setTimeout(() => setCopyStatus(false), 2000);
};

const bomData = (activeTab === "bom" ? feBomData : beBomData) as CycloneDXBOM;
const packages = bomData?.sbom?.packages || [];
Comment on lines +25 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript interfaces for the GitHub SBOM format.

The code lacks type definitions for the new GitHub SBOM format, which could lead to runtime errors.

Add these type definitions:

interface GitHubSBOM {
  sbom: {
    spdxVersion: string;
    creationInfo: {
      created: string;
    };
    packages: Array<{
      name: string;
      versionInfo?: string;
      licenseConcluded?: string;
      externalRefs?: Array<{
        referenceLocator?: string;
        referenceCategory?: string;
      }>;
    }>;
  };
}

const bomData = (activeTab === "bom" ? feBomData : beBomData) as GitHubSBOM;
const packages = bomData?.sbom?.packages || [];


return (
<div className="p-4">
Expand All @@ -84,73 +40,68 @@ const BOMDisplay: React.FC = () => {
}`}
onClick={() => setActiveTab("bom")}
>
Care Frontend
{t("Care Frontend")}
</button>
<button
className={`text-md w-full rounded-md px-4 py-2 transition-all duration-300 md:w-auto ${
activeTab === "beBom" ? "bg-primary text-white" : "bg-gray-200"
}`}
onClick={() => setActiveTab("beBom")}
>
Care Backend
{t("Care Backend")}
</button>
</div>
<Card className="rounded-lg bg-white p-4 shadow-md transition-all duration-300">
<div className="mb-4">
<h2 className="mb-2 text-xl font-semibold text-primary md:text-2xl">
{bomData.bomFormat || "N/A"} BOM (Version:{" "}
{bomData.version || "N/A"})
{t("SPDX SBOM (Version: {{version}})", {
version: bomData?.sbom?.spdxVersion || t("N/A"),
})}
</h2>
<p className="text-sm text-gray-500">
Created on:{" "}
{bomData.metadata?.timestamp
? dayjs(bomData.metadata.timestamp).format("MMMM D, YYYY")
: "N/A"}
{t("Created on: {{date}}", {
date: bomData?.sbom?.creationInfo?.created
? dayjs(bomData.sbom.creationInfo.created).format(
"MMMM D, YYYY",
)
: t("N/A"),
})}
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<h3 className="col-span-full text-lg font-semibold text-primary">
Components:
{t("Packages:")}
</h3>
{bomData.components?.map((component, index) => (
{packages.map((pkg, index) => (
<div
key={index}
className="block rounded-md border p-2 transition-all duration-300 hover:shadow-lg"
>
<a
href={
component.externalReferences?.[1]?.url ||
component.externalReferences?.[0]?.url ||
"#"
}
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary-dark block text-primary"
>
<strong className="text-lg">
{component.name || "N/A"} v{component.version || "N/A"}
{t("{{name}} v{{version}}", {
name: pkg.name || t("N/A"),
version: pkg.versionInfo || t("N/A"),
})}
</strong>
</a>
{component.licenses && component.licenses[0]?.license?.id && (
{pkg.licenseConcluded && (
<p className="text-base">
License:{" "}
{t("License:")}{" "}
<a
href={
getLicenseUrl(component.licenses[0].license.id) || "#"
}
href={getLicenseUrl(pkg.licenseConcluded) || "#"}
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary-dark text-primary"
>
{component.licenses[0].license.id || "N/A"}
{pkg.licenseConcluded || t("N/A")}
</a>
</p>
)}
{component.description && (
<p className="text-base">
Description: {component.description}
</p>
)}
<div>
<h4
className="block cursor-pointer font-semibold text-primary"
Expand All @@ -164,15 +115,21 @@ const BOMDisplay: React.FC = () => {
</h4>
{showExternalRefs === index && (
<ul className="list-inside list-disc pl-4 text-xs">
{component.externalReferences?.map((ref, idx) => (
{pkg.externalRefs?.map((ref, idx) => (
<li key={idx}>
<a
href={ref.url || "#"}
href={ref.referenceLocator || "#"}
className="hover:text-primary-dark block break-words text-primary"
>
{ref.url || "N/A"}
{ref.referenceLocator || t("N/A")}
</a>
{ref.comment && <p>Comment: {ref.comment}</p>}
{ref.referenceCategory && (
<p>
{t("Category: {{category}}", {
category: ref.referenceCategory,
})}
</p>
)}
</li>
))}
</ul>
Expand All @@ -187,12 +144,12 @@ const BOMDisplay: React.FC = () => {
onCopy={handleCopy}
>
<button className="text-md hover:bg-primary-dark w-full rounded-md bg-primary px-4 py-2 text-white transition-all duration-300 focus:outline-none md:w-auto">
Copy BOM JSON
{t("Copy BOM JSON")}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not the way to add translations. see how other translations are done

</button>
</CopyToClipboard>
{copyStatus && (
<span className="mt-2 block text-sm text-gray-600">
Copied to clipboard!
{t("Copied to clipboard!")}
</span>
)}
</div>
Expand Down
Loading