From efddad8ec1286bf5257f928efe3b02e855b52ba3 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 13 Apr 2024 10:10:00 +0800 Subject: [PATCH 1/5] feat: allow GitHub usernames for SSH public keys In the form of `gh:username`. --- README.md | 2 ++ config.yaml | 2 ++ src/Lhp/Remote.hs | 66 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 643161f..15d1eb7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ plain host name. The configuration file looks like as follows: ## config.yaml ## List of known SSH public keys for all hosts. knownSshKeys: + - gh:some-github-user - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS test-key-admin ## List of hosts to patrol @@ -119,6 +120,7 @@ hosts: cost: 50 ## List of known SSH public keys for the host (optional) knownSshKeys: + - gh:another-github-user - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGmlBxUagOqtWcW6B77TUL8li85ZNYx0tphd3TSx4SEB test-key-tenant - name: otherhost url: https://internal.documentation/hosts/otherhost diff --git a/config.yaml b/config.yaml index b5ca4ec..bc3abbe 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,6 @@ ## List of known SSH public keys for all hosts. knownSshKeys: + - gh:some-github-user - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS test-key-admin ## List of hosts to patrol @@ -26,6 +27,7 @@ hosts: cost: 50 ## List of known SSH public keys for the host (optional) knownSshKeys: + - gh:another-github-user - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGmlBxUagOqtWcW6B77TUL8li85ZNYx0tphd3TSx4SEB test-key-tenant - name: otherhost url: https://internal.documentation/hosts/otherhost diff --git a/src/Lhp/Remote.hs b/src/Lhp/Remote.hs index dba0220..bdf6814 100644 --- a/src/Lhp/Remote.hs +++ b/src/Lhp/Remote.hs @@ -44,7 +44,7 @@ compileReport -> m Types.Report compileReport par Config.Config {..} = do _reportHosts <- reporter _configHosts - _reportKnownSshKeys <- mapM parseSshPublicKey _configKnownSshKeys + _reportKnownSshKeys <- concat <$> mapM parseSshPublicKeys _configKnownSshKeys pure Types.Report {..} where reporter = bool (fmap catMaybes . mapM go) (MP.mapM compileHostReport) par @@ -74,7 +74,7 @@ compileHostReport ch = do _hostReportKernel <- _mkKernel _hostName kvs _hostReportDistribution <- _mkDistribution _hostName kvs _hostReportDockerContainers <- _fetchHostDockerContainers h - _hostReportAuthorizedSshKeys <- _fetchHostAuthorizedSshKeys h >>= mapM parseSshPublicKey + _hostReportAuthorizedSshKeys <- _fetchHostAuthorizedSshKeys h >>= fmap concat . mapM parseSshPublicKeys _hostReportSystemdServices <- _fetchHostSystemdServices h _hostReportSystemdTimers <- _fetchHostSystemdTimers h pure Types.HostReport {..} @@ -94,7 +94,7 @@ _makeHostFromConfigHostSpec Config.HostSpec {..} = _hostTags = _hostSpecTags _hostData = _hostSpecData in do - _hostKnownSshKeys <- mapM parseSshPublicKey _hostSpecKnownSshKeys + _hostKnownSshKeys <- concat <$> mapM parseSshPublicKeys _hostSpecKnownSshKeys pure Types.Host {..} @@ -417,16 +417,38 @@ _toSshError h = _modifyError (LhpErrorSsh h) --- | Creates 'Types.SshPublicKey' from given 'T.Text' using ssh-keygen. +-- | Creates list of 'Types.SshPublicKey' from given 'T.Text' using @ssh-keygen@. -- --- >>> runExceptT $ parseSshPublicKey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3" --- Right (SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "no comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}) --- >>> runExceptT $ parseSshPublicKey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 comment" --- Right (SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}) --- >>> runExceptT $ parseSshPublicKey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment" --- Right (SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "some more comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}) --- >>> runExceptT $ parseSshPublicKey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment" --- Right (SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "some more comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}) +-- If the given 'T.Text' is a GitHub username, it will attempt to +-- fetch keys from GitHub and then parse them using @ssh-keygen@. +-- +-- >>> runExceptT $ parseSshPublicKeys "gh:vst" +-- Right [SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJIQtEmoHu44pUDwX5GEw20JLmfZaI+xVXin74GI396z", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "gh:vst", _sshPublicKeyFingerprint = "MD5:01:6d:4f:ca:c9:ca:dc:f1:cb:a3:fc:74:8e:34:77:16"},SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "gh:vst", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}] +-- >>> runExceptT $ parseSshPublicKeys "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3" +-- Right [SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "no comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}] +-- >>> runExceptT $ parseSshPublicKeys "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 comment" +-- Right [SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}] +-- >>> runExceptT $ parseSshPublicKeys "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment" +-- Right [SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "some more comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}] +-- >>> runExceptT $ parseSshPublicKeys "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment" +-- Right [SshPublicKey {_sshPublicKeyData = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3 some more comment", _sshPublicKeyType = "ED25519", _sshPublicKeyLength = 256, _sshPublicKeyComment = "some more comment", _sshPublicKeyFingerprint = "MD5:ec:4b:ff:8d:c7:43:a9:ab:16:9f:0d:fa:8f:e2:6f:6c"}] +parseSshPublicKeys + :: MonadError LhpError m + => MonadIO m + => T.Text + -> m [Types.SshPublicKey] +parseSshPublicKeys s = do + let gh = "gh:" + if T.isPrefixOf gh s + then do + let u = T.drop (T.length gh) s + ks <- listGitHubSshKeys u + fmap (\x -> x {Types._sshPublicKeyComment = s}) <$> mapM parseSshPublicKey ks + else List.singleton <$> parseSshPublicKey s + + +-- | Attempts to create 'Types.SshPublicKey' from given SSH public key +-- represented as 'T.Text' using @ssh-keygen@. parseSshPublicKey :: MonadError LhpError m => MonadIO m @@ -435,7 +457,7 @@ parseSshPublicKey parseSshPublicKey s = do (ec, out, err) <- TP.readProcess process case ec of - ExitFailure _ -> throwUnknown (Z.Text.unsafeTextFromBL err) + ExitFailure _ -> throwUnknown (Z.Text.unsafeTextFromBL err <> ". Input was: " <> s) ExitSuccess -> case T.words (Z.Text.unsafeTextFromBL out) of (l : fp : r) -> pure $ @@ -451,3 +473,21 @@ parseSshPublicKey s = do throwUnknown = throwError . LhpErrorUnknown stdin = TP.byteStringInput (Z.Text.blFromText s) process = TP.setStdin stdin (TP.proc "ssh-keygen" ["-E", "md5", "-l", "-f", "-"]) + + +-- | Attempts to get the list of SSH public keys from GitHub for a +-- given GitHub username. +listGitHubSshKeys + :: MonadError LhpError m + => MonadIO m + => T.Text + -> m [T.Text] +listGitHubSshKeys u = do + (ec, out, err) <- TP.readProcess process + case ec of + ExitFailure _ -> throwUnknown (Z.Text.unsafeTextFromBL err) + ExitSuccess -> pure (toKeys out) + where + throwUnknown = throwError . LhpErrorUnknown + process = TP.proc "curl" ["-s", "https://github.com/" <> T.unpack u <> ".keys"] + toKeys = filter (not . T.null) . T.lines . Z.Text.unsafeTextFromBL From 5b543fe614f86558993969a35f642e8215997559 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 13 Apr 2024 10:40:01 +0800 Subject: [PATCH 2/5] feat: report public SSH host keys on host Relates to #63. --- src/Lhp/Remote.hs | 14 ++++++++++++++ src/Lhp/Types.hs | 2 ++ src/scripts/ssh-host-keys.sh | 14 ++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 src/scripts/ssh-host-keys.sh diff --git a/src/Lhp/Remote.hs b/src/Lhp/Remote.hs index bdf6814..a217fdc 100644 --- a/src/Lhp/Remote.hs +++ b/src/Lhp/Remote.hs @@ -74,6 +74,7 @@ compileHostReport ch = do _hostReportKernel <- _mkKernel _hostName kvs _hostReportDistribution <- _mkDistribution _hostName kvs _hostReportDockerContainers <- _fetchHostDockerContainers h + _hostReportPublicSshHostKeys <- _fetchHostPublicSshHostKeys h >>= fmap concat . mapM parseSshPublicKeys _hostReportAuthorizedSshKeys <- _fetchHostAuthorizedSshKeys h >>= fmap concat . mapM parseSshPublicKeys _hostReportSystemdServices <- _fetchHostSystemdServices h _hostReportSystemdTimers <- _fetchHostSystemdTimers h @@ -169,6 +170,19 @@ _fetchHostDockerContainers h@Types.Host {..} = Right sv -> pure sv +-- | Attempts to find and return all public SSH host keys on the remote +-- host. +_fetchHostPublicSshHostKeys + :: MonadIO m + => MonadError LhpError m + => Types.Host + -> m [T.Text] +_fetchHostPublicSshHostKeys h@Types.Host {..} = + filter (not . T.null) . fmap T.strip . T.lines . Z.Text.unsafeTextFromBL <$> prog + where + prog = _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/ssh-host-keys.sh") ["bash"]) + + -- | Attempts to find and return all SSH authorized keys on the remote -- host. _fetchHostAuthorizedSshKeys diff --git a/src/Lhp/Types.hs b/src/Lhp/Types.hs index 2ec7166..415c3ee 100644 --- a/src/Lhp/Types.hs +++ b/src/Lhp/Types.hs @@ -85,6 +85,7 @@ data HostReport = HostReport , _hostReportKernel :: !Kernel , _hostReportDistribution :: !Distribution , _hostReportDockerContainers :: !(Maybe [DockerContainer]) + , _hostReportPublicSshHostKeys :: ![SshPublicKey] , _hostReportAuthorizedSshKeys :: ![SshPublicKey] , _hostReportSystemdServices :: ![T.Text] , _hostReportSystemdTimers :: ![T.Text] @@ -108,6 +109,7 @@ instance ADC.HasCodec HostReport where <*> ADC.requiredField "kernel" "Kernel information." ADC..= _hostReportKernel <*> ADC.requiredField "distribution" "Distribution information." ADC..= _hostReportDistribution <*> ADC.requiredField "dockerContainers" "List of Docker containers if the host is a Docker host." ADC..= _hostReportDockerContainers + <*> ADC.requiredField "publicSshHostKeys" "List of public SSH host keys found on host." ADC..= _hostReportPublicSshHostKeys <*> ADC.requiredField "authorizedSshKeys" "List of authorized SSH public keys found on host." ADC..= _hostReportAuthorizedSshKeys <*> ADC.requiredField "systemdServices" "List of systemd services found on host." ADC..= _hostReportSystemdServices <*> ADC.requiredField "systemdTimers" "List of systemd timers found on host." ADC..= _hostReportSystemdTimers diff --git a/src/scripts/ssh-host-keys.sh b/src/scripts/ssh-host-keys.sh new file mode 100644 index 0000000..cbf901e --- /dev/null +++ b/src/scripts/ssh-host-keys.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +################### +# SHELL BEHAVIOUR # +################### + +# Stop on errors: +set -e + +############# +# PROCEDURE # +############# + +find "/etc/ssh" -iname 'ssh_host_*.pub' -exec cat {} \; From abe163cee7113238f0c91fa0ba92b80889a81961 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 13 Apr 2024 10:46:46 +0800 Subject: [PATCH 3/5] feat(website): tabulate public SSH host keys on host details Closes #63. --- .../src/components/report/ShowHostDetails.tsx | 26 +++++++++++++++++++ website/src/lib/data.ts | 22 ++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx index 4359020..666d06d 100644 --- a/website/src/components/report/ShowHostDetails.tsx +++ b/website/src/components/report/ShowHostDetails.tsx @@ -140,6 +140,32 @@ export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: Lhp + + Public SSH Host Keys + + + No public SSH host keys are found. Sounds weird? + } + > + {({ length, type, fingerprint, data, comment }) => ( + { + navigator.clipboard.writeText(data); + toast('SSH Key is copied to clipboard.'); + }} + > + {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} + + )} + + + + Docker Containers diff --git a/website/src/lib/data.ts b/website/src/lib/data.ts index 3ccad7b..f243890 100644 --- a/website/src/lib/data.ts +++ b/website/src/lib/data.ts @@ -190,6 +190,27 @@ export const LHP_PATROL_REPORT_SCHEMA = { required: ['os', 'machine', 'version', 'release', 'name', 'node'], type: 'object', }, + publicSshHostKeys: { + $comment: 'List of public SSH host 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', + }, systemdServices: { $comment: 'List of systemd services found on host.', items: { type: 'string' }, @@ -206,6 +227,7 @@ export const LHP_PATROL_REPORT_SCHEMA = { 'systemdTimers', 'systemdServices', 'authorizedSshKeys', + 'publicSshHostKeys', 'dockerContainers', 'distribution', 'kernel', From 3ee0c42ce1250e2416619ec28c6bd93df61b515a Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Sat, 13 Apr 2024 11:49:01 +0800 Subject: [PATCH 4/5] feat(website): show hardware info on host details component --- website/src/components/helpers.tsx | 4 +- .../src/components/report/ShowHostDetails.tsx | 46 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/website/src/components/helpers.tsx b/website/src/components/helpers.tsx index 9b5d54c..ccd7ea5 100644 --- a/website/src/components/helpers.tsx +++ b/website/src/components/helpers.tsx @@ -18,12 +18,14 @@ export function BigSpinner({ label }: { label?: string }) { export function KVBox({ title, kvs, + ...rest }: { title: string; kvs: { key: string; value: React.ReactNode | string | number | null | undefined }[]; + [prop: string]: any; }) { return ( - + {title} diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx index 666d06d..11afd69 100644 --- a/website/src/components/report/ShowHostDetails.tsx +++ b/website/src/components/report/ShowHostDetails.tsx @@ -47,21 +47,33 @@ export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: Lhp - +
+ + + +
No public SSH host keys are found. Sounds weird? - } + emptyContent={No public SSH host keys are found. Sounds weird?} > {({ length, type, fingerprint, data, comment }) => ( Date: Sat, 13 Apr 2024 22:47:12 +0800 Subject: [PATCH 5/5] feat(website): improve SSH public keys section on host details component --- .../src/components/report/ShowHostDetails.tsx | 164 ++++++++++++------ 1 file changed, 115 insertions(+), 49 deletions(-) diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx index 11afd69..01cb439 100644 --- a/website/src/components/report/ShowHostDetails.tsx +++ b/website/src/components/report/ShowHostDetails.tsx @@ -1,15 +1,24 @@ -import { LhpHostReport, LhpPatrolReport } from '@/lib/data'; -import { Card, CardBody, CardHeader } from '@nextui-org/card'; -import { Chip } from '@nextui-org/chip'; -import { Listbox, ListboxItem } from '@nextui-org/listbox'; +import { LhpHostReport, LhpPatrolReport, SshPublicKey } from '@/lib/data'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + Chip, + Listbox, + ListboxItem, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from '@nextui-org/react'; import Link from 'next/link'; import { toast } from 'react-toastify'; import { KVBox } from '../helpers'; export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: LhpPatrolReport }) { - const authorizedKeysPlanned = [...(data.knownSshKeys || []), ...(host.host.knownSshKeys || [])]; - const authorizedKeysPlannedSet = new Set(authorizedKeysPlanned.map((x) => x.fingerprint)); - return (

@@ -102,54 +111,30 @@ export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: Lhp

- Authorized SSH Keys Found + Authorized SSH Public Keys - No authorized SSH keys are found. Sounds weird?} - > - {({ length, type, fingerprint, data, comment }) => ( - 🟢 : <>🔴} - onPress={() => { - navigator.clipboard.writeText(data); - toast('SSH Key is copied to clipboard.'); - }} - > - {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} - - )} - + - - - Authorized SSH Keys Planned + + { + const keys = Object.values( + [...(host.host.knownSshKeys || []), ...(data.knownSshKeys || [])].reduce( + (acc, x) => ({ ...acc, [`${x.fingerprint}`]: x }), + {} as Record + ) + ).sort((a, b) => (a.comment || '').localeCompare(b.comment || '')); - - No authorized SSH keys are found as planned. Sounds weird? - } + navigator.clipboard.writeText(keys.map((x) => `${x.data} ${x.comment}`).join('\n')); + toast('SSH public keys are copied to clipboard.'); + }} > - {({ length, type, fingerprint, data, comment }) => ( - { - navigator.clipboard.writeText(data); - toast('SSH Key is copied to clipboard.'); - }} - > - {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} - - )} -
- + (copy known keys) + + @@ -226,3 +211,84 @@ export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: Lhp
); } + +export function TabulateSshKeys({ host, data }: { host: LhpHostReport; data: LhpPatrolReport }) { + const keysKnownGlobal = (data.knownSshKeys || []).reduce( + (acc, x) => ({ ...acc, [`${x.fingerprint}`]: x }), + {} as Record + ); + const keysKnownHost = (host.host.knownSshKeys || []).reduce( + (acc, x) => ({ ...acc, [`${x.fingerprint}`]: x }), + {} as Record + ); + const keysSeen = (host.authorizedSshKeys || []).reduce( + (acc, x) => ({ ...acc, [`${x.fingerprint}`]: x }), + {} as Record + ); + const keys = Object.values(keysKnownGlobal).concat(Object.values(keysKnownHost), Object.values(keysSeen)); + const fps = Array.from(new Set(keys.map((x) => x.fingerprint))).map((fingerprint) => ({ fingerprint })); + + return ( + + + Type + Length + Known? + Fingerprint + Comments + + + + {(record) => { + const fp = record.fingerprint; + + const keyG = keysKnownGlobal[fp]; + const keyH = keysKnownHost[fp]; + const keyS = keysSeen[fp]; + const key = (keyG || keyH || keyS) as SshPublicKey; + const known: 'global' | 'host' | 'unknown' = keyG ? 'global' : keyH ? 'host' : 'unknown'; + const comments = Array.from(new Set(keys.filter((x) => x.fingerprint === fp).map((x) => x.comment))); + + return ( + + {key.type} + {key.length} + + {known} + + + {key.fingerprint} +
+ { + navigator.clipboard.writeText(key.fingerprint); + toast('SSH public key fingerprint is copied to clipboard.'); + }} + > + (copy fingerprint) + + + { + navigator.clipboard.writeText(key.data); + toast('SSH public key is copied to clipboard.'); + }} + > + (copy key) + +
+
+ + {comments.map((c) => ( + {c} + ))} + +
+ ); + }} +
+
+ ); +}