diff --git a/.gitignore b/.gitignore index 7d093c3..9160a27 100644 --- a/.gitignore +++ b/.gitignore @@ -8,31 +8,23 @@ # testing /coverage -# next.js -/.next/ -/out/ - # production /build +/.next # misc .DS_Store -*.pem +.env.local +.env.development.local +.env.test.local +.env.production.local -# debug npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local +**/.idea/ -# vercel -.vercel +**/.vscode/ -# typescript -*.tsbuildinfo +**/.DS_Store diff --git a/.prettierignore b/.prettierignore index 62abd50..09a4a71 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ build* node_modules* .next* +src/modules/rekor/types/* diff --git a/package.json b/package.json index 3253d4c..d3eb267 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-dom": "^18.0.0-rc.1", "react-error-boundary": "^3.1.4", "react-highlight": "^0.14.0", + "react-hook-form": "^7.31.0", "react": "^18.0.0-rc.1", "rxjs": "^7.5.4", "sass": "^1.49.9", diff --git a/src/modules/rekor/api/rekor_api.ts b/src/modules/rekor/api/rekor_api.ts index dcef7d8..233f44f 100644 --- a/src/modules/rekor/api/rekor_api.ts +++ b/src/modules/rekor/api/rekor_api.ts @@ -1,11 +1,13 @@ -import { combineLatest, Observable, of } from "rxjs"; +import { combineLatest, from, Observable, of } from "rxjs"; import { fromFetch } from "rxjs/fetch"; import { map, switchMap } from "rxjs/operators"; export interface RekorIndexQuery { email?: string; - artifact?: string; - sha?: string; + hash?: string; + commitSha?: string; + uuid?: string; + logIndex?: string; } export interface RekorEntry { @@ -37,9 +39,11 @@ function retrieveIndex(query: RekorIndexQuery): Observable { ); } -function retrieveEntries(logIndex: string) { +function retrieveEntries(entryUUID?: string, logIndex?: string) { return fromFetch( - `https://rekor.sigstore.dev/api/v1/log/entries/${logIndex}`, + `https://rekor.sigstore.dev/api/v1/log/entries${ + entryUUID ? "/" + entryUUID : "" + }?logIndex=${logIndex ?? ""}`, { headers: { "Content-Type": "application/json", @@ -57,10 +61,51 @@ function retrieveEntries(logIndex: string) { ); } +async function digestMessage(message: string) { + const msgUint8 = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); +} + +async function buildIndexQuery(query: RekorIndexQuery) { + return { + hash: + query.hash ?? (query.commitSha && (await digestMessage(query.commitSha))), + email: query.email, + }; +} + export function rekorRetrieve( query: RekorIndexQuery ): Observable { - return retrieveIndex(query).pipe( + if (query.uuid || query.logIndex) { + return retrieveEntries(query.uuid, query.logIndex).pipe( + map(result => { + const [key, value] = Object.entries(result)[0]; + return { + totalCount: 1, + entries: [ + { + key, + content: value, + }, + ], + }; + }) + ); + } + + return from(buildIndexQuery(query)).pipe( + map(query => { + if ((query.hash?.length ?? 0) > 0) { + if (!query.hash?.startsWith("sha256:")) { + query.hash = `sha256:${query.hash}`; + } + } + return query; + }), + switchMap(params => retrieveIndex(params)), map((logIndexes: string[]) => ({ totalCount: logIndexes.length, indexes: logIndexes.slice(0, 20), diff --git a/src/modules/rekor/components/rekor_explorer.tsx b/src/modules/rekor/components/rekor_explorer.tsx index dccdc09..0fe7510 100644 --- a/src/modules/rekor/components/rekor_explorer.tsx +++ b/src/modules/rekor/components/rekor_explorer.tsx @@ -1,3 +1,4 @@ +import { InputOutlined } from "@mui/icons-material"; import { Alert, Box, CircularProgress, Typography } from "@mui/material"; import { bind, Subscribe } from "@react-rxjs/core"; import { createSignal, suspend } from "@react-rxjs/utils"; @@ -15,7 +16,7 @@ import { } from "rxjs/operators"; import { useDestroyed$ } from "../../utils/rxjs"; import { RekorIndexQuery, rekorRetrieve } from "../api/rekor_api"; -import { RekorSearchForm } from "./search_form"; +import { FormInputs, RekorSearchForm } from "./search_form"; const [queryChange$, setQuery] = createSignal(); @@ -121,9 +122,17 @@ export function LoadingIndicator() { } export function RekorExplorer() { + function createQueryFromFormInput(input: FormInputs): RekorIndexQuery { + return { + [input.type]: input.value, + }; + } + return (
- setQuery(query)} /> + setQuery(createQueryFromFormInput(query))} + /> }> diff --git a/src/modules/rekor/components/search_form.tsx b/src/modules/rekor/components/search_form.tsx index 4d676fe..20e6df8 100644 --- a/src/modules/rekor/components/search_form.tsx +++ b/src/modules/rekor/components/search_form.tsx @@ -1,22 +1,98 @@ -import { Button, Divider, Grid, TextField, Typography } from "@mui/material"; +import { + Button, + FormControl, + Grid, + InputLabel, + MenuItem, + Select, + TextField, +} from "@mui/material"; import Paper from "@mui/material/Paper"; -import { useState } from "react"; +import { useMemo } from "react"; +import { Controller, RegisterOptions, useForm } from "react-hook-form"; export interface FormProps { - onSubmit: (query: { email: string; artifact: string }) => void; + onSubmit: (query: FormInputs) => void; } +const TYPES = ["email", "hash", "commitSha", "uuid", "logIndex"] as const; + +export interface FormInputs { + type: typeof TYPES[number]; + value: string; +} + +type Rules = Omit< + RegisterOptions, + "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled" +>; + +const nameByType: Record = { + email: "Email", + hash: "Hash", + commitSha: "Commit SHA", + uuid: "Entry UUID", + logIndex: "Log Index", +}; +const rulesByType: Record = { + email: { + pattern: { + value: /\S+@\S+\.\S+/, + message: "Entered value does not match the email format: 'S+@S+.S+'", + }, + }, + hash: { + pattern: { + value: /^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$/, + message: + "Entered value does not match the hash format: '^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$'", + }, + }, + commitSha: { + pattern: { + value: /^[0-9a-fA-F]{40}$/, + message: + "Entered value does not match the commit SHA format: '^[0-9a-fA-F]{40}$'", + }, + }, + uuid: { + pattern: { + value: /^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$/, + message: + "Entered value does not match the entry UUID format: '^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$'", + }, + }, + logIndex: { + min: { + value: 0, + message: "Entered value must be larger than 0", + }, + }, +}; + export function RekorSearchForm({ onSubmit }: FormProps) { - const [email, setEmail] = useState(""); - const [artifact, setArtifact] = useState(""); + const { handleSubmit, control, watch } = useForm({ + mode: "all", + defaultValues: { + type: "email", + value: "", + }, + }); + + const watchType = watch("type"); + + const rules = Object.assign( + { + required: { + value: true, + message: "A value is required", + }, + }, + rulesByType[watchType] + ); return ( -
{ - e.preventDefault(); - onSubmit({ email, artifact }); - }} - > + + ( + + Field + + + )} + /> + + - { - setEmail(event.target.value); - }} + ( + + )} /> diff --git a/src/modules/theme/theme.ts b/src/modules/theme/theme.ts index 3124dd0..1424e4c 100644 --- a/src/modules/theme/theme.ts +++ b/src/modules/theme/theme.ts @@ -1,5 +1,4 @@ import { createTheme } from "@mui/material/styles"; -// import InterFont from './inter.ttf'; export const REKOR_SEARCH_THEME = createTheme({ typography: { diff --git a/yarn.lock b/yarn.lock index 528b1b5..168cafa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -480,7 +480,7 @@ "@peculiar/x509@^1.6.1": version "1.6.1" - resolved "https://registry.npmjs.org/@peculiar/x509/-/x509-1.6.1.tgz" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-1.6.1.tgz#cc33807ab481824c69145e884cb40012aec501b0" integrity sha512-C4oxpCuYasfjuhy6QRFJhs0R6gyeQSRsB7MsT6JkO3qaFi4b75mm8hNEKa+sIJPtTjXCC94tW9rHx1hw5dOvnQ== dependencies: "@peculiar/asn1-cms" "^2.0.44" @@ -2235,6 +2235,11 @@ react-highlight@^0.14.0: dependencies: highlight.js "^10.5.0" +react-hook-form@^7.31.0: + version "7.31.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.31.0.tgz#a36d4fd257e08bbb7c141b29159df1eb2f1a3979" + integrity sha512-dDZSOk0eRf8z2EFt/XVA+HnmffxMYSWQaVX5EXCp2ozYtqyqrCDdfQRVBoVC43YyKIDtWfbiRcFkMvnvxv9q5g== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"