diff --git a/website/src/components/app/-app.tsx b/website/src/components/app/-app.tsx index d93746f..2ae8c88 100644 --- a/website/src/components/app/-app.tsx +++ b/website/src/components/app/-app.tsx @@ -8,17 +8,17 @@ import Link from 'next/link'; import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { LhpData } from './-data'; +import { LhpHostReport, LhpPatrolReport } from './-data'; import { KVBox } from './-ui'; -export function App({ data, onFlushRequest }: { data: LhpData[]; onFlushRequest: () => void }) { - const [host, setHost] = useState>(Nothing); +export function App({ data, onFlushRequest }: { data: LhpPatrolReport; onFlushRequest: () => void }) { + const [host, setHost] = useState>(Nothing); return (
setHost(Just(x))} onFlushRequest={onFlushRequest} onTabulateRequest={() => { @@ -29,7 +29,7 @@ export function App({ data, onFlushRequest }: { data: LhpData[]; onFlushRequest:
{host.caseOf({ - Nothing: () => setHost(Just(x))} />, + Nothing: () => setHost(Just(x))} />, Just: (x) => , })}
@@ -38,8 +38,8 @@ export function App({ data, onFlushRequest }: { data: LhpData[]; onFlushRequest: } export interface SidebarProps { - data: LhpData[]; - onHostSelect: (host: LhpData) => void; + data: LhpHostReport[]; + onHostSelect: (host: LhpHostReport) => void; onFlushRequest: () => void; onTabulateRequest: () => void; } @@ -100,9 +100,15 @@ export function cloudIcon(x: string) { } } -export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHostSelect: (host: LhpData) => void }) { - const [filters, setFilters] = useState boolean>>({}); - const [filteredHosts, setFilteredHosts] = useState(hosts); +export function TabulateHosts({ + hosts, + onHostSelect, +}: { + hosts: LhpHostReport[]; + onHostSelect: (host: LhpHostReport) => void; +}) { + const [filters, setFilters] = useState boolean>>({}); + const [filteredHosts, setFilteredHosts] = useState(hosts); useEffect(() => { setFilteredHosts(hosts.filter((host) => Object.values(filters).reduce((acc, f) => acc && f(host), true))); @@ -236,20 +242,20 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos sshkeys: x === 'all' || x.size === 0 ? () => true - : (h) => h.sshAuthorizedKeys.reduce((acc, t) => acc || x.has(t), false), + : (h) => h.authorizedSshKeys.reduce((acc, t) => acc || x.has(t.fingerprint), false), }); }} > {hosts - .map((h) => h.sshAuthorizedKeys || []) + .map((h) => h.authorizedSshKeys || []) .reduce((acc, tags) => [...acc, ...tags], []) .sort() .filter(function (el, i, a) { return i === a.indexOf(el); }) .map((n) => ( - - {n} + + {n.data} ))} @@ -451,7 +457,7 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos ? '❌' : `${host.dockerContainers.filter((x) => x.running).length} / ${host.dockerContainers.length}`} - {host.sshAuthorizedKeys.length} + {host.authorizedSshKeys.length} {host.systemdServices.length} / {host.systemdTimers.length} @@ -470,9 +476,7 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos ); } -export function HostDetails({ host }: { host: LhpData }) { - const sshkeys = host.sshAuthorizedKeys.map((x) => [x, ...x.split(' ')]); - +export function HostDetails({ host }: { host: LhpHostReport }) { return (

@@ -544,19 +548,19 @@ export function HostDetails({ host }: { host: LhpData }) { No authorized SSH keys are found. Sounds weird?} > - {([sshkey, sshkeyType, _sshkeyContent, ...sshkeyComment]) => ( + {({ length, type, fingerprint, data, comment }) => ( { - navigator.clipboard.writeText(sshkey); + navigator.clipboard.writeText(data); toast('SSH Key is copied to clipboard.'); }} > - {`${sshkeyType} ${sshkeyComment.join(' ') || ''}`} + {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} )} diff --git a/website/src/components/app/-data.tsx b/website/src/components/app/-data.tsx index f4e954d..619989d 100644 --- a/website/src/components/app/-data.tsx +++ b/website/src/components/app/-data.tsx @@ -11,146 +11,218 @@ import { Centered } from './-ui'; export const LHP_PATROL_REPORT_SCHEMA = { $comment: 'Host Patrol Report\nReport', properties: { - cloud: { - $comment: 'Cloud information.\nCloud Information\nCloud', - properties: { - hostAvailabilityZone: { $comment: 'Host availability zone.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostLocalAddress: { $comment: 'Local address of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostLocalHostname: { $comment: 'Local hostname of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostRegion: { $comment: 'Host region.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostRemoteAddress: { $comment: 'Remote address of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostRemoteHostname: { $comment: 'Remote hostname of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - hostReservedAddress: { - $comment: 'Reserved address of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], - }, - hostType: { $comment: 'Host type.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - id: { $comment: 'Host identifier.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - name: { $comment: 'Cloud name.', type: 'string' }, - }, - required: [ - 'hostReservedAddress', - 'hostRemoteAddress', - 'hostRemoteHostname', - 'hostLocalAddress', - 'hostLocalHostname', - 'hostAvailabilityZone', - 'hostRegion', - 'hostType', - 'id', - 'name', - ], - type: 'object', - }, - distribution: { - $comment: 'Distribution information.\nDistribution Information\nDistribution', - properties: { - codename: { - $comment: "Distribution codename (cat /etc/os-release | grep 'VERSION_CODENAME=').", - anyOf: [{ type: 'null' }, { type: 'string' }], - }, - description: { - $comment: "Distribution description (cat /etc/os-release | grep 'PRETTY_NAME=').", - type: 'string', - }, - id: { $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').", type: 'string' }, - name: { $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).", type: 'string' }, - release: { $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').", type: 'string' }, - version: { $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').", type: 'string' }, - }, - required: ['description', 'codename', 'release', 'version', 'name', 'id'], - type: 'object', - }, - dockerContainers: { - $comment: 'List of Docker containers if the host is a Docker host.', - anyOf: [ - { type: 'null' }, - { - items: { - $comment: 'Docker Container Information\nDockerContainer', + hosts: { + $comment: 'List of host reports.', + items: { + $comment: 'Host Patrol Report\nReport', + properties: { + authorizedSshKeys: { + $comment: 'List of authorized SSH public keys found on host.', + items: { + $comment: 'SSH Public Key Information\nSshPublicKey', + properties: { + comment: { $comment: 'Comment on the public key.', type: 'string' }, + data: { $comment: 'Original information.', type: 'string' }, + fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, + length: { + $comment: 'Length of the public key.', + maximum: 2147483647, + minimum: -2147483648, + type: 'number', + }, + type: { $comment: 'Type of the public key.', type: 'string' }, + }, + required: ['fingerprint', 'comment', 'length', 'type', 'data'], + type: 'object', + }, + type: 'array', + }, + cloud: { + $comment: 'Cloud information.\nCloud Information\nCloud', properties: { - created: { $comment: 'Date/time when the container is created at.\nLocalTime', type: 'string' }, - id: { $comment: 'ID of the container..', type: 'string' }, - image: { $comment: 'Image the container is created from.', type: 'string' }, - name: { $comment: 'Name of the container.', type: 'string' }, - running: { $comment: 'Indicates if the container is running.', type: 'boolean' }, + hostAvailabilityZone: { + $comment: 'Host availability zone.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostLocalAddress: { + $comment: 'Local address of the host.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostLocalHostname: { + $comment: 'Local hostname of the host.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostRegion: { $comment: 'Host region.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + hostRemoteAddress: { + $comment: 'Remote address of the host.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostRemoteHostname: { + $comment: 'Remote hostname of the host.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostReservedAddress: { + $comment: 'Reserved address of the host.', + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + hostType: { $comment: 'Host type.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + id: { $comment: 'Host identifier.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + name: { $comment: 'Cloud name.', type: 'string' }, }, - required: ['running', 'created', 'image', 'name', 'id'], + required: [ + 'hostReservedAddress', + 'hostRemoteAddress', + 'hostRemoteHostname', + 'hostLocalAddress', + 'hostLocalHostname', + 'hostAvailabilityZone', + 'hostRegion', + 'hostType', + 'id', + 'name', + ], type: 'object', }, - type: 'array', - }, - ], - }, - hardware: { - $comment: 'Hardware information.\nRudimentary Hardware Information\nHardware', - properties: { - cpuCount: { - $comment: 'Number of (v)CPU cores.', - // maximum: 9223372036854775807, - // minimum: -9223372036854775808, - type: 'number', + distribution: { + $comment: 'Distribution information.\nDistribution Information\nDistribution', + properties: { + codename: { + $comment: "Distribution codename (cat /etc/os-release | grep 'VERSION_CODENAME=').", + anyOf: [{ type: 'null' }, { type: 'string' }], + }, + description: { + $comment: "Distribution description (cat /etc/os-release | grep 'PRETTY_NAME=').", + type: 'string', + }, + id: { $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').", type: 'string' }, + name: { $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).", type: 'string' }, + release: { $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').", type: 'string' }, + version: { $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').", type: 'string' }, + }, + required: ['description', 'codename', 'release', 'version', 'name', 'id'], + type: 'object', + }, + dockerContainers: { + $comment: 'List of Docker containers if the host is a Docker host.', + anyOf: [ + { type: 'null' }, + { + items: { + $comment: 'Docker Container Information\nDockerContainer', + properties: { + created: { $comment: 'Date/time when the container is created at.\nLocalTime', type: 'string' }, + id: { $comment: 'ID of the container..', type: 'string' }, + image: { $comment: 'Image the container is created from.', type: 'string' }, + name: { $comment: 'Name of the container.', type: 'string' }, + running: { $comment: 'Indicates if the container is running.', type: 'boolean' }, + }, + required: ['running', 'created', 'image', 'name', 'id'], + type: 'object', + }, + type: 'array', + }, + ], + }, + hardware: { + $comment: 'Hardware information.\nRudimentary Hardware Information\nHardware', + properties: { + cpuCount: { + $comment: 'Number of (v)CPU cores.', + maximum: 2147483647, + minimum: -2147483648, + type: 'number', + }, + diskRoot: { $comment: 'Total disk space of root (`/`) filesystem (in GB).', type: 'number' }, + ramTotal: { $comment: 'Total RAM (in GB).', type: 'number' }, + }, + required: ['diskRoot', 'ramTotal', 'cpuCount'], + type: 'object', + }, + host: { + $comment: 'Host descriptor.\nHost Descriptor\nHost', + properties: { + name: { $comment: 'Name of the host.', type: 'string' }, + tags: { $comment: 'Arbitrary tags for the host.', items: { type: 'string' }, type: 'array' }, + url: { $comment: 'URL to external host information.', type: 'string' }, + }, + required: ['name'], + type: 'object', + }, + kernel: { + $comment: 'Kernel information.\nKernel Information\nKernel', + properties: { + machine: { $comment: 'Architecture the kernel is running on (uname -m).', type: 'string' }, + name: { $comment: 'Kernel name (uname -s).', type: 'string' }, + node: { $comment: 'Name of the node kernel is running on (uname -n).', type: 'string' }, + os: { $comment: 'Operating system the kernel is driving (uname -o).', type: 'string' }, + release: { $comment: 'Kernel release (uname -r).', type: 'string' }, + version: { $comment: 'Kernel version (uname -v).', type: 'string' }, + }, + required: ['os', 'machine', 'version', 'release', 'name', 'node'], + type: 'object', + }, + systemdServices: { + $comment: 'List of systemd services found on host.', + items: { type: 'string' }, + type: 'array', + }, + systemdTimers: { + $comment: 'List of systemd timers found on host.', + items: { type: 'string' }, + type: 'array', + }, }, - diskRoot: { $comment: 'Total disk space of root (`/`) filesystem (in GB).', type: 'number' }, - ramTotal: { $comment: 'Total RAM (in GB).', type: 'number' }, + required: [ + 'systemdTimers', + 'systemdServices', + 'authorizedSshKeys', + 'dockerContainers', + 'distribution', + 'kernel', + 'hardware', + 'cloud', + 'host', + ], + type: 'object', }, - required: ['diskRoot', 'ramTotal', 'cpuCount'], - type: 'object', - }, - host: { - $comment: 'Host descriptor.\nHost Descriptor\nHost', - properties: { - name: { $comment: 'Name of the host.', type: 'string' }, - tags: { $comment: 'Arbitrary tags for the host.', items: { type: 'string' }, type: 'array' }, - url: { $comment: 'URL to external host information.', type: 'string' }, - }, - required: ['name'], - type: 'object', + type: 'array', }, - kernel: { - $comment: 'Kernel information.\nKernel Information\nKernel', - properties: { - machine: { $comment: 'Architecture the kernel is running on (uname -m).', type: 'string' }, - name: { $comment: 'Kernel name (uname -s).', type: 'string' }, - node: { $comment: 'Name of the node kernel is running on (uname -n).', type: 'string' }, - os: { $comment: 'Operating system the kernel is driving (uname -o).', type: 'string' }, - release: { $comment: 'Kernel release (uname -r).', type: 'string' }, - version: { $comment: 'Kernel version (uname -v).', type: 'string' }, + knownSshKeys: { + $comment: 'List of known SSH public keys.', + items: { + $comment: 'SSH Public Key Information\nSshPublicKey', + properties: { + comment: { $comment: 'Comment on the public key.', type: 'string' }, + data: { $comment: 'Original information.', type: 'string' }, + fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, + length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number' }, + type: { $comment: 'Type of the public key.', type: 'string' }, + }, + required: ['fingerprint', 'comment', 'length', 'type', 'data'], + type: 'object', }, - required: ['os', 'machine', 'version', 'release', 'name', 'node'], - type: 'object', - }, - sshAuthorizedKeys: { - $comment: 'List of SSH authorized keys found on host.', - items: { type: 'string' }, type: 'array', }, - systemdServices: { $comment: 'List of systemd services found on host.', items: { type: 'string' }, type: 'array' }, - systemdTimers: { $comment: 'List of systemd timers found on host.', items: { type: 'string' }, type: 'array' }, }, - required: [ - 'systemdTimers', - 'systemdServices', - 'sshAuthorizedKeys', - 'dockerContainers', - 'distribution', - 'kernel', - 'hardware', - 'cloud', - 'host', - ], + required: ['knownSshKeys', 'hosts'], type: 'object', } as const satisfies JSONSchema; -export type LhpData = FromSchema; +export type LhpPatrolReport = FromSchema; + +export type ArrayElement = ArrayType extends readonly (infer ElementType)[] + ? ElementType + : never; + +export type LhpHostReport = ArrayElement; const AJV = new Ajv(); -const LHP_PATROL_REPORT_VALIDATOR = AJV.compile(LHP_PATROL_REPORT_SCHEMA); +const LHP_PATROL_REPORT_VALIDATOR = AJV.compile(LHP_PATROL_REPORT_SCHEMA); const _LOCAL_STORAGE_KEY_DATA = 'LHP_DATA'; -export function loadData(): Either> { +export function loadData(): Either> { const data = localStorage.getItem(_LOCAL_STORAGE_KEY_DATA); if (data === null) { @@ -160,29 +232,14 @@ export function loadData(): Either> { return parseData(data).map(Just); } -export function saveData(x: LhpData[]): void { - localStorage.setItem(_LOCAL_STORAGE_KEY_DATA, JSON.stringify(x)); -} - -export function deleteData(): void { - localStorage.removeItem(_LOCAL_STORAGE_KEY_DATA); -} - -export function parseData(raw: string): Either { +export function parseData(data: string): Either { try { - const parsed = JSON.parse(raw); + const parsed = JSON.parse(data); + const result = LHP_PATROL_REPORT_VALIDATOR(parsed); - if (!Array.isArray(parsed)) { - return Left('Data is expected to be an array of host information objects.'); - } - - for (const elem of parsed) { - const result = LHP_PATROL_REPORT_VALIDATOR(elem); - - if (!result) { - console.error(elem, LHP_PATROL_REPORT_VALIDATOR.errors); - return Left('Invalid host information object.'); - } + if (!result) { + console.error(LHP_PATROL_REPORT_VALIDATOR.errors); + return Left('Invalid lhp patrol report object.'); } return Right(parsed); @@ -191,7 +248,15 @@ export function parseData(raw: string): Either { } } -export function DataLoader({ onLoadData }: { onLoadData: (x: LhpData[]) => void }) { +export function saveData(x: LhpPatrolReport): void { + localStorage.setItem(_LOCAL_STORAGE_KEY_DATA, JSON.stringify(x)); +} + +export function deleteData(): void { + localStorage.removeItem(_LOCAL_STORAGE_KEY_DATA); +} + +export function DataLoader({ onLoadData }: { onLoadData: (x: LhpPatrolReport) => void }) { const [error, setError] = useState(); const changeHandler = (e: ChangeEvent) => { diff --git a/website/src/components/app/index.tsx b/website/src/components/app/index.tsx index 71d094b..9f02e39 100644 --- a/website/src/components/app/index.tsx +++ b/website/src/components/app/index.tsx @@ -3,11 +3,11 @@ import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; import { useEffect, useState } from 'react'; import { App } from './-app'; -import { DataLoader, LhpData, deleteData, loadData } from './-data'; +import { DataLoader, LhpPatrolReport, deleteData, loadData } from './-data'; import { BigSpinner } from './-ui'; export function AppMain() { - const [data, setAppData] = useState>>(Nothing); + const [data, setAppData] = useState>>(Nothing); useEffect(() => { loadData().caseOf({