diff --git a/explorer/src/components/account/SecurityCard.tsx b/explorer/src/components/account/SecurityCard.tsx
new file mode 100644
index 0000000000..5a6119c982
--- /dev/null
+++ b/explorer/src/components/account/SecurityCard.tsx
@@ -0,0 +1,280 @@
+import { ErrorCard } from "components/common/ErrorCard";
+import { TableCardBody } from "components/common/TableCardBody";
+import { UpgradeableLoaderAccountData } from "providers/accounts";
+import { fromProgramData, SecurityTXT } from "utils/security-txt";
+
+export function SecurityCard({ data }: { data: UpgradeableLoaderAccountData }) {
+ if (!data.programData) {
+ return ;
+ }
+
+ const { securityTXT, error } = fromProgramData(data.programData);
+ if (!securityTXT) {
+ return ;
+ }
+
+ return (
+
Last Deployed Slot |
@@ -165,6 +177,21 @@ export function UpgradeableProgramSection({
);
}
+function SecurityLabel() {
+ return (
+
+
+ Security.txt
+
+
+
+ );
+}
+
function LastVerifiedBuildLabel() {
return (
diff --git a/explorer/src/components/common/SecurityTXTBadge.tsx b/explorer/src/components/common/SecurityTXTBadge.tsx
new file mode 100644
index 0000000000..e98c75c664
--- /dev/null
+++ b/explorer/src/components/common/SecurityTXTBadge.tsx
@@ -0,0 +1,33 @@
+import { PublicKey } from "@solana/web3.js";
+import { Link } from "react-router-dom";
+import { fromProgramData } from "utils/security-txt";
+import { clusterPath } from "utils/url";
+import { ProgramDataAccountInfo } from "validators/accounts/upgradeable-program";
+
+export function SecurityTXTBadge({
+ programData,
+ pubkey,
+}: {
+ programData: ProgramDataAccountInfo;
+ pubkey: PublicKey;
+}) {
+ const { securityTXT, error } = fromProgramData(programData);
+ if (securityTXT) {
+ return (
+
+
+ Included
+
+
+ );
+ } else {
+ return (
+
+ {error}
+
+ );
+ }
+}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index c433f8ab5e..fb84739e3b 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -40,6 +40,7 @@ import { MetaplexMetadataCard } from "components/account/MetaplexMetadataCard";
import { NFTHeader } from "components/account/MetaplexNFTHeader";
import { DomainsCard } from "components/account/DomainsCard";
import isMetaplexNFT from "providers/accounts/utils/isMetaplexNFT";
+import { SecurityCard } from "components/account/SecurityCard";
const IDENTICON_WIDTH = 64;
@@ -108,6 +109,13 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = {
path: "/stake-history",
},
],
+ "bpf-upgradeable-loader": [
+ {
+ slug: "security",
+ title: "Security",
+ path: "/security",
+ },
+ ],
};
const TOKEN_TABS_HIDDEN = [
@@ -319,7 +327,8 @@ export type MoreTabs =
| "instructions"
| "rewards"
| "metadata"
- | "domains";
+ | "domains"
+ | "security";
function MoreSection({
account,
@@ -389,6 +398,9 @@ function MoreSection({
/>
)}
{tab === "domains" && }
+ {tab === "security" && data?.program === "bpf-upgradeable-loader" && (
+
+ )}
>
);
}
diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss
index a5029afd1b..75321c76e3 100644
--- a/explorer/src/scss/_solana.scss
+++ b/explorer/src/scss/_solana.scss
@@ -445,3 +445,33 @@ p.updated-time {
text-overflow: ellipsis;
white-space: nowrap;
}
+
+// security-txt css hacks
+.security-txt ul {
+ list-style: none;
+ text-align: right;
+ margin: 0;
+ padding-inline-start: 0;
+}
+
+.security-txt p, pre {
+ text-align: left !important;
+ margin: 0;
+}
+
+.security-txt a {
+ white-space: nowrap;
+}
+
+.security-txt td {
+ white-space: unset;
+}
+
+.security-txt code {
+ white-space: pre-wrap;
+ display: block;
+}
+
+.security-txt-link-color-hack-reee {
+ color: white;
+}
\ No newline at end of file
diff --git a/explorer/src/utils/security-txt.ts b/explorer/src/utils/security-txt.ts
new file mode 100644
index 0000000000..099b909acc
--- /dev/null
+++ b/explorer/src/utils/security-txt.ts
@@ -0,0 +1,100 @@
+import { ProgramDataAccountInfo } from "validators/accounts/upgradeable-program";
+
+export type SecurityTXT = {
+ name: string;
+ project_url: string;
+ contacts: string;
+ policy: string;
+ preferred_languages?: string;
+ source_code?: string;
+ encryption?: string;
+ auditors?: string;
+ acknowledgements?: string;
+ expiry?: string;
+};
+const REQUIRED_KEYS: (keyof SecurityTXT)[] = [
+ "name",
+ "project_url",
+ "contacts",
+ "policy",
+];
+const VALID_KEYS: (keyof SecurityTXT)[] = [
+ "name",
+ "project_url",
+ "contacts",
+ "policy",
+ "preferred_languages",
+ "source_code",
+ "encryption",
+ "auditors",
+ "acknowledgements",
+ "expiry",
+];
+
+const HEADER = "=======BEGIN SECURITY.TXT V1=======\0";
+const FOOTER = "=======END SECURITY.TXT V1=======\0";
+
+export const fromProgramData = (
+ programData: ProgramDataAccountInfo
+): { securityTXT?: SecurityTXT; error?: string } => {
+ const [data, encoding] = programData.data;
+ if (!(data && encoding === "base64"))
+ return { securityTXT: undefined, error: "Failed to decode program data" };
+
+ const decoded = Buffer.from(data, encoding);
+
+ const headerIdx = decoded.indexOf(HEADER);
+ const footerIdx = decoded.indexOf(FOOTER);
+
+ if (headerIdx < 0 || footerIdx < 0) {
+ return { securityTXT: undefined, error: "Program has no security.txt" };
+ }
+
+ /*
+ the expected structure of content should be a list
+ of ascii encoded key value pairs seperated by null characters.
+ e.g. key1\0value1\0key2\0value2\0
+ */
+ const content = decoded.subarray(headerIdx + HEADER.length, footerIdx);
+
+ const map = content
+ .reduce(
+ (prev, current) => {
+ if (current === 0) {
+ prev.push([]);
+ } else {
+ prev[prev.length - 1].push(current);
+ }
+ return prev;
+ },
+ [[]]
+ )
+ .map((c) => String.fromCharCode(...c))
+ .reduce<{ map: { [key: string]: string }; key: string | undefined }>(
+ (prev, current) => {
+ const key = prev.key;
+ if (!key) {
+ return {
+ map: prev.map,
+ key: current,
+ };
+ } else {
+ return {
+ map: {
+ ...(VALID_KEYS.some((x) => x === key) ? { [key]: current } : {}),
+ ...prev.map,
+ },
+ key: undefined,
+ };
+ }
+ },
+ { map: {}, key: undefined }
+ ).map;
+ if (!REQUIRED_KEYS.every((k) => k in map)) {
+ return {
+ securityTXT: undefined,
+ error: `some required fields (${REQUIRED_KEYS}) are missing`,
+ };
+ }
+ return { securityTXT: map as SecurityTXT, error: undefined };
+};
|