Skip to content

Commit

Permalink
Allow searching by more attributes (#10)
Browse files Browse the repository at this point in the history
* Add more fields to the query explorer

* Ignore generated files

* Refactor the form to rely on a dropdown + value field combo
  • Loading branch information
pdeslaur authored May 18, 2022
1 parent d2df9fd commit c12f2c1
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 50 deletions.
24 changes: 8 additions & 16 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
build*
node_modules*
.next*
src/modules/rekor/types/*
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
57 changes: 51 additions & 6 deletions src/modules/rekor/api/rekor_api.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -37,9 +39,11 @@ function retrieveIndex(query: RekorIndexQuery): Observable<string[]> {
);
}

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",
Expand All @@ -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<RekorEntries> {
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),
Expand Down
13 changes: 11 additions & 2 deletions src/modules/rekor/components/rekor_explorer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<RekorIndexQuery>();

Expand Down Expand Up @@ -121,9 +122,17 @@ export function LoadingIndicator() {
}

export function RekorExplorer() {
function createQueryFromFormInput(input: FormInputs): RekorIndexQuery {
return {
[input.type]: input.value,
};
}

return (
<div>
<RekorSearchForm onSubmit={query => setQuery(query)} />
<RekorSearchForm
onSubmit={query => setQuery(createQueryFromFormInput(query))}
/>

<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<LoadingIndicator />}>
Expand Down
163 changes: 139 additions & 24 deletions src/modules/rekor/components/search_form.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,166 @@
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<FormInputs["type"], string> = {
email: "Email",
hash: "Hash",
commitSha: "Commit SHA",
uuid: "Entry UUID",
logIndex: "Log Index",
};
const rulesByType: Record<FormInputs["type"], Rules> = {
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<FormInputs>({
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 (
<form
onSubmit={e => {
e.preventDefault();
onSubmit({ email, artifact });
}}
>
<form onSubmit={handleSubmit(onSubmit)}>
<Paper sx={{ p: 2 }}>
<Grid
container
spacing={2}
>
<Grid
item
xs
md={6}
xs={4}
md={2}
>
<Controller
name="type"
control={control}
render={({ field }) => (
<FormControl
fullWidth
size="small"
>
<InputLabel id="rekor-search-type-label">Field</InputLabel>
<Select
labelId="rekor-search-type-label"
id="rekor-search-type"
{...field}
label="Field"
>
{TYPES.map(value => (
<MenuItem
key={value}
value={value}
>
{nameByType[value]}
</MenuItem>
))}
</Select>
</FormControl>
)}
/>
</Grid>
<Grid
item
xs={8}
md={8}
>
<TextField
sx={{ width: 1 }}
id="email"
label="Email"
size="small"
variant="outlined"
value={email}
onChange={event => {
setEmail(event.target.value);
}}
<Controller
name="value"
control={control}
rules={rules}
render={({ field, fieldState }) => (
<TextField
sx={{ width: 1 }}
size="small"
{...field}
label={nameByType[watchType]}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Grid>
<Grid
item
xs="auto"
xs={12}
md={2}
>
<Button
type="submit"
variant="contained"
fullWidth
>
Search
</Button>
Expand Down
1 change: 0 additions & 1 deletion src/modules/theme/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createTheme } from "@mui/material/styles";
// import InterFont from './inter.ttf';

export const REKOR_SEARCH_THEME = createTheme({
typography: {
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

1 comment on commit c12f2c1

@vercel
Copy link

@vercel vercel bot commented on c12f2c1 May 18, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

rekor-search-ui – ./

rekor.tlog.dev
rekor-search-ui-git-main.chainguard.app
rekor-search-ui.vercel.app
rekor-search-ui.chainguard.app

Please sign in to comment.