From a008e396d41d3a00a3bd31bc6145b9b4ce06142b Mon Sep 17 00:00:00 2001 From: hunterino-sec <41126746+hunterino-sec@users.noreply.github.com> Date: Tue, 12 May 2026 18:35:38 +0200 Subject: [PATCH] Added credentials panel and management --- src/apollo/operations.ts | 71 +++ src/components/CommandBar/CommandBar.tsx | 5 +- src/components/CommandBar/FileTaskModal.tsx | 70 ++- .../CredentialsPanel.module.css | 511 ++++++++++++++++ .../CredentialsPanel/CredentialsPanel.tsx | 573 ++++++++++++++++++ src/views/Dashboard.tsx | 8 +- 6 files changed, 1222 insertions(+), 16 deletions(-) create mode 100644 src/components/CredentialsPanel/CredentialsPanel.module.css create mode 100644 src/components/CredentialsPanel/CredentialsPanel.tsx diff --git a/src/apollo/operations.ts b/src/apollo/operations.ts index 0ede89d..a5a997b 100644 --- a/src/apollo/operations.ts +++ b/src/apollo/operations.ts @@ -543,6 +543,7 @@ export const GET_COMMANDS = gql` default_value choices parameter_group_name + limit_credentials_by_type } } } @@ -958,3 +959,73 @@ export const GET_JOB_KILL_COMMAND = gql` } } ` + +// ── Credentials ─────────────────────────────────────── + +const CREDENTIAL_FIELDS = gql` + fragment CredentialFields on credential { + id type account realm credential_text comment metadata timestamp deleted + operator { username } + task { display_id callback { host display_id } } + } +` + +export const GET_CREDENTIALS = gql` + ${CREDENTIAL_FIELDS} + query GetCredentials { + credential( + where: { deleted: { _eq: false } } + order_by: { timestamp: desc } + ) { ...CredentialFields } + } +` + +export const SUB_CREDENTIALS = gql` + ${CREDENTIAL_FIELDS} + subscription SubCredentials($now: timestamp!) { + credential_stream( + batch_size: 50 + cursor: { initial_value: { timestamp: $now } } + ) { ...CredentialFields } + } +` + +export const CREATE_CREDENTIAL = gql` + mutation CreateCredential( + $credential_type: String, $account: String, $realm: String, + $credential: String, $comment: String + ) { + createCredential( + credential_type: $credential_type, account: $account, realm: $realm, + credential: $credential, comment: $comment + ) { + status + error + } + } +` + +export const UPDATE_CREDENTIAL = gql` + ${CREDENTIAL_FIELDS} + mutation UpdateCredential( + $id: Int!, $type: String!, $account: String!, $realm: String!, + $credential: bytea!, $comment: String!, $metadata: String! + ) { + update_credential_by_pk( + pk_columns: { id: $id } + _set: { + type: $type, account: $account, realm: $realm, + credential_raw: $credential, comment: $comment, metadata: $metadata + } + ) { ...CredentialFields } + } +` + +export const DELETE_CREDENTIAL = gql` + mutation DeleteCredential($id: Int!) { + update_credential_by_pk( + pk_columns: { id: $id } + _set: { deleted: true } + ) { id deleted } + } +` diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx index d72cf88..efefedf 100644 --- a/src/components/CommandBar/CommandBar.tsx +++ b/src/components/CommandBar/CommandBar.tsx @@ -134,8 +134,11 @@ export function CommandBar() { const hasRequiredFile = cmdParams.some( p => (p.type === 'File' || p.type === 'FileMultiple') && p.required ) + const hasRequiredCred = cmdParams.some( + p => p.type === 'CredentialJson' && p.required + ) - if (hasRequiredFile) { + if (hasRequiredFile || hasRequiredCred) { // Open modal — it handles upload + task creation setHistory(h => [raw, ...h.slice(0, 99)]) setHistIdx(-1) diff --git a/src/components/CommandBar/FileTaskModal.tsx b/src/components/CommandBar/FileTaskModal.tsx index f21f1fd..048524f 100644 --- a/src/components/CommandBar/FileTaskModal.tsx +++ b/src/components/CommandBar/FileTaskModal.tsx @@ -5,19 +5,30 @@ ═══════════════════════════════════════════════════ */ import { useState, useRef } from 'react' -import { useMutation } from '@apollo/client' -import { CREATE_TASK } from '@/apollo/operations' +import { useMutation, useQuery } from '@apollo/client' +import { CREATE_TASK, GET_CREDENTIALS } from '@/apollo/operations' import { useStore } from '@/store' import styles from './FileTaskModal.module.css' +interface CredentialOption { + id: number + type: string + account: string + realm: string + credential_text: string | null + comment: string + metadata: string +} + export interface CommandParam { - name: string - display_name: string - type: string - required: boolean - default_value: string | null - choices: string[] | null - parameter_group_name:string + name: string + display_name: string + type: string + required: boolean + default_value: string | null + choices: string[] | null + parameter_group_name: string + limit_credentials_by_type: string[] | null } interface Props { @@ -69,10 +80,11 @@ async function uploadTaskFile(file: File, token: string): Promise export function FileTaskModal({ command, params, displayId, defaultCwd, onClose }: Props) { const { token } = useStore() - // Pick the parameter group that contains the File-type param. - // Sending params from multiple groups causes Mythic to reject with "don't match any parameter group". + // Pick the group containing a File or CredentialJson param — sending params from multiple + // groups causes Mythic to reject with "don't match any parameter group". + const credParam = params.find(p => p.type === 'CredentialJson') const fileParam = params.find(p => p.type === 'File' || p.type === 'FileMultiple') - const activeGroup = fileParam?.parameter_group_name ?? 'Default Parameter Group' + const activeGroup = (credParam ?? fileParam)?.parameter_group_name ?? 'Default Parameter Group' const groupParams = params.filter(p => p.parameter_group_name === activeGroup) // Within that group: skip None (crypto), skip filename params (auto-populated from file.name), dedupe by name. @@ -109,6 +121,10 @@ export function FileTaskModal({ command, params, displayId, defaultCwd, onClose const [createTask] = useMutation(CREATE_TASK) + const hasCredentialParam = visibleParams.some(p => p.type === 'CredentialJson') + const { data: credData } = useQuery(GET_CREDENTIALS, { skip: !hasCredentialParam }) + const credentials: CredentialOption[] = credData?.credential ?? [] + function setValue(name: string, val: string) { setValues(v => ({ ...v, [name]: val })) } @@ -182,6 +198,8 @@ export function FileTaskModal({ command, params, displayId, defaultCwd, onClose } else if (p.type === 'Array' || p.type === 'TypedArray' || p.type === 'ChooseMultiple') { paramsObj[p.name] = (values[p.name] ?? '') .split(',').map(s => s.trim()).filter(Boolean) + } else if (p.type === 'CredentialJson') { + try { paramsObj[p.name] = JSON.parse(values[p.name] ?? '{}') } catch { paramsObj[p.name] = {} } } else if (p.type !== 'None') { paramsObj[p.name] = values[p.name] ?? '' } @@ -290,6 +308,34 @@ export function FileTaskModal({ command, params, displayId, defaultCwd, onClose ))} + ) : p.type === 'CredentialJson' ? ( + + ) : ( = { + hash: 'var(--crimson-400)', + plaintext: '#6aaa64', + certificate: '#6b8fd4', + key: '#d4916b', + ticket: '#b06bd4', + cookie: '#4ec9b0', +} + +function typeColor(type: string): string { + return TYPE_COLORS[type.toLowerCase()] ?? 'var(--bone-600)' +} + +function fmtAgo(iso: string): string { + const diff = Date.now() - parseTs(iso).getTime() + if (diff < 60_000) return 'just now' + if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago` + if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago` + return parseTs(iso).toLocaleDateString([], { month: 'short', day: 'numeric' }) +} + +function maskCredential(val: string): string { + if (!val) return '—' + return '•'.repeat(Math.min(val.length, 36)) + (val.length > 36 ? '…' : '') +} + +const KNOWN_TYPES = ['hash', 'plaintext', 'certificate', 'key', 'ticket', 'cookie'] + +// ── AddModal ────────────────────────────────────────── + +interface AddModalProps { + loading: boolean + onClose: () => void + onAdd: (form: CredForm) => void +} + +interface CredForm { + type: string + account: string + realm: string + credential_text: string + comment: string + metadata: string +} + +function AddModal({ loading, onClose, onAdd }: AddModalProps) { + const [form, setForm] = useState({ + type: 'hash', account: '', realm: '', credential_text: '', comment: '', metadata: '', + }) + + function set(k: keyof CredForm, v: string) { setForm(p => ({ ...p, [k]: v })) } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!form.account.trim() || !form.credential_text.trim()) return + onAdd(form) + } + + return ( +
e.target === e.currentTarget && onClose()}> +
+
+ Add Credential + +
+ +
+ + + + + set('account', e.target.value)} + placeholder="username" + required + autoFocus + /> + + + set('realm', e.target.value)} + placeholder="domain or host" + /> + + +