Skip to content

Commit d87d7ad

Browse files
authored
feat: Add support for external variables (#102)
* bug: filter out variables during manual step and add dictionaries to types * feat: add support for external variables when running a playbook * refactor: remove the suspense card in favour of spinner it was quite annoying that nothing would be accessible if soarca was not reachabble. Also the skeleton cards were quite ugly, I changed it to a simple spinner.
1 parent 0e653d9 commit d87d7ad

21 files changed

Lines changed: 906 additions & 752 deletions

mock-playbooks/block-ad-user.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"type": "playbook",
3+
"spec_version": "cacao-2.0",
4+
"id": "playbook--4e286394-ff8f-487f-84dd-f7ccf1fa59de",
5+
"name": "Block AD user",
6+
"description": "Blocks AD user by providing the AD user name",
7+
"created_by": "identity--aaacfc1a-fa46-4c1c-85dd-2a0823ce3943",
8+
"created": "2024-09-04T08:19:39.424Z",
9+
"modified": "2024-09-04T08:22:25.712Z",
10+
"revoked": false,
11+
"playbook_variables": {
12+
"__user__": {
13+
"type": "string",
14+
"constant": false,
15+
"external": true
16+
}
17+
},
18+
"workflow_start": "start--4270ba65-31c3-4041-9fa0-17241122bb9f",
19+
"workflow": {
20+
"start--4270ba65-31c3-4041-9fa0-17241122bb9f": {
21+
"on_completion": "action--0a6fbf98-4e75-45fd-9948-8c62908521db",
22+
"type": "start"
23+
},
24+
"action--0a6fbf98-4e75-45fd-9948-8c62908521db": {
25+
"name": "Disable account",
26+
"description": "Disable windows account in AD",
27+
"on_completion": "end--bfbcfc10-1926-4409-8ea2-bfd1814365b3",
28+
"type": "action",
29+
"commands": [
30+
{
31+
"type": "powershell",
32+
"description": "Block user given to the playbook",
33+
"command": "Disable-ADAccount -Identity __user__:value"
34+
}
35+
],
36+
"agent": "soarca--49cde8bd-cc50-4d1a-b60c-8d49a785f4b9",
37+
"targets": ["net-address--78a788f6-c2b8-4c46-9648-c2cb933c63b2"]
38+
},
39+
"end--bfbcfc10-1926-4409-8ea2-bfd1814365b3": {
40+
"type": "end"
41+
}
42+
},
43+
"authentication_info_definitions": {
44+
"authentication-info--9a2d2fd6-aa6d-4d2e-ad2a-f6f84d544dbc": {
45+
"type": "user-auth",
46+
"name": "Domain admin",
47+
"username": "admin.project",
48+
"password": "censored",
49+
"kms": false
50+
}
51+
},
52+
"agent_definitions": {
53+
"soarca--49cde8bd-cc50-4d1a-b60c-8d49a785f4b9": {
54+
"type": "soarca",
55+
"name": "soarca-powershell"
56+
}
57+
},
58+
"target_definitions": {
59+
"net-address--78a788f6-c2b8-4c46-9648-c2cb933c63b2": {
60+
"type": "net-address",
61+
"name": "Windows AD DS server",
62+
"address": {
63+
"ipv4": ["192.168.1.121"]
64+
},
65+
"port": "5985",
66+
"authentication_info": "authentication-info--9a2d2fd6-aa6d-4d2e-ad2a-f6f84d544dbc"
67+
}
68+
}
69+
}

src/api/manual.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { Execution, ManualOutArgsUpdatePayload } from "@/types";
2-
import { HttpMutationMethod, mutationToApi } from "./utils";
1+
import {
2+
Execution,
3+
InteractionCommandData,
4+
ManualOutArgsUpdatePayload,
5+
} from "@/types";
6+
import { fetchFromApi, HttpMutationMethod, mutationToApi } from "./utils";
37

48
export const postStepActionResult = (data: ManualOutArgsUpdatePayload) =>
59
mutationToApi<Execution>(
610
HttpMutationMethod.POST,
711
`/api/manual/continue`,
812
data,
913
);
14+
15+
export const getStepManualData = (executionId: string, stepId: string) =>
16+
fetchFromApi<InteractionCommandData>(`/api/manual/${executionId}/${stepId}`);

src/api/trigger.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { Execution } from "@/types";
1+
import { Execution, Variables } from "@/types";
22
import { HttpMutationMethod, mutationToApi } from "./utils";
33

4-
export const triggerPlaybookById = (playbookId: string) => {
4+
export const triggerPlaybookById = (
5+
playbookId: string,
6+
variables?: Variables,
7+
) => {
58
return mutationToApi<Execution>(
69
HttpMutationMethod.POST,
710
`/api/trigger/playbook/${playbookId}`,
8-
{},
11+
variables ?? {},
912
);
1013
};

src/api/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export async function deleteToApi(url: string): Promise<void> {
7777
* @example
7878
* const errorResponse = apiErrorToErrorResponse(error);
7979
*/
80-
export const getErrorFromApiResponse = (error: Error): ErrorResponse => {
81-
if (error && error instanceof Error) {
80+
export const getErrorFromApiResponse = (error: unknown): ErrorResponse => {
81+
if (error instanceof Error) {
8282
try {
8383
return JSON.parse(error.message) as ErrorResponse;
8484
} catch {
@@ -90,14 +90,14 @@ export const getErrorFromApiResponse = (error: Error): ErrorResponse => {
9090

9191
/**
9292
* Formats an error for display in a toast notification.
93-
* @param error - The Error object to format
93+
* @param error - The error to format (any value; non-Error inputs use the default message)
9494
* @param defaultMessage - The default message to use if the error does not have a specific message
9595
* @returns Formatted error message string
9696
* @example
9797
* const message = formatErrorForToast(error, "An unexpected error occurred.");
9898
*/
9999
export const formatErrorForToast = (
100-
error: Error,
100+
error: unknown,
101101
defaultMessage: string,
102102
): string => {
103103
const parsed = getErrorFromApiResponse(error);

src/components/Containers.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,10 @@ export const PageContainer = styled.div<PageContainerProps>`
3434
3535
padding: 0;
3636
`;
37+
38+
export const CenteredCardContent = styled.div`
39+
display: flex;
40+
align-items: center;
41+
justify-content: center;
42+
min-height: 12rem;
43+
`;

src/components/CopyButton.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Copy } from "lucide-react";
22
import React from "react";
33
import toast from "react-hot-toast";
44

5+
import { formatErrorForToast } from "@/api/utils";
56
import { Button } from "./Button";
67
import Icon from "./Icon";
78
import { ThemeSize } from "./utils";
@@ -23,8 +24,6 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
2324
disabled = false,
2425
...rest
2526
}) => {
26-
27-
2827
const handleClick = async (e: React.MouseEvent) => {
2928
e.stopPropagation();
3029
if (disabled || !$text) return;
@@ -33,8 +32,7 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
3332
await navigator.clipboard.writeText($text);
3433
toast.success("Copied to clipboard");
3534
} catch (err) {
36-
console.error("Copy failed", err);
37-
toast.error("Failed to copy to clipboard");
35+
toast.error(formatErrorForToast(err, "Failed to copy to clipboard"));
3836
}
3937
};
4038

src/components/Form.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const Input = styled.input`
1818
1919
padding: ${({ theme }) => theme.spacing.md} ${({ theme }) => theme.spacing.md};
2020
21-
font: ${({ theme }) => theme.typography.caption.font};
21+
font: ${({ theme }) => theme.typography.body.font};
2222
color: ${({ theme }) => theme.colors.text.primary};
2323
background: ${({ theme }) => theme.colors.background.primary};
2424
@@ -53,16 +53,24 @@ export const Input = styled.input`
5353
const StyledSelect = styled.select`
5454
width: 100%;
5555
box-sizing: border-box;
56+
appearance: none;
57+
-webkit-appearance: none;
5658
5759
border: 1px solid ${({ theme }) => theme.colors.border.medium};
5860
border-radius: ${({ theme }) => theme.radius.md};
5961
60-
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
62+
padding: ${({ theme }) =>
63+
`${theme.spacing.md} ${theme.spacing["2xl"]} ${theme.spacing.md} ${theme.spacing.md}`};
6164
6265
font: ${({ theme }) => theme.typography.body.font};
6366
color: ${({ theme }) => theme.colors.text.primary};
64-
background: ${({ theme }) => theme.colors.background.primary};
65-
line-height: 1.5;
67+
background-color: ${({ theme }) => theme.colors.background.primary};
68+
background-image: ${({ theme }) => {
69+
const color = theme.colors.text.tertiary.replace("#", "%23");
70+
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='${color}' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C%2Fsvg%3E")`;
71+
}};
72+
background-repeat: no-repeat;
73+
background-position: right ${({ theme }) => theme.spacing.md} center;
6674
6775
transition: border-color ${({ theme }) => theme.transitions.base};
6876
@@ -80,7 +88,7 @@ const StyledSelect = styled.select`
8088
&:disabled {
8189
opacity: 0.5;
8290
cursor: not-allowed;
83-
background: ${({ theme }) => theme.colors.background.secondary};
91+
background-color: ${({ theme }) => theme.colors.background.secondary};
8492
}
8593
8694
& > option {

src/components/cards/SuspenseCard.tsx

Lines changed: 0 additions & 154 deletions
This file was deleted.

src/components/cards/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from "./Card";
22
export * from "./ExpandableCard";
3-
export * from "./SuspenseCard";

0 commit comments

Comments
 (0)