617 add velo data store only button (#630)
* feat: add data_store_only option to CollectArtifactBody for conditional result retrieval * feat: add data_store_only checkbox to ArtifactsCollect for optimized artifact storage * precommit-fixes * chore: update CURRENT_VERSION to 0.1.30
taylor_socfortress committed
Jan 24, 2026 at 16:25 UTC
35a8b35aaa84b595ffc64c7bc27e4eb86e20d3af
5 files changed
+137
-93
backend/app/connectors/velociraptor/schema/artifacts.py
+5
@@ -138,6 +138,10 @@ class CollectArtifactBody(BaseBody):
138
None,
139
description="Optional parameters for the artifact, such as environment variables",
140
)
141
+ data_store_only: Optional[bool] = Field(
142
+ False,
143
+ description="If true, only store the collected data in the datastore without sending it back immediately",
144
+ )
145
146
class Config:
147
schema_extra = {
@@ -147,6 +151,7 @@ class CollectArtifactBody(BaseBody):
151
"velociraptor_org": "root",
152
"artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
153
"parameters": {"env": [{"key": "InstallART", "value": "N"}, {"key": "T1552.001 - 3", "value": "Y"}]},
154
+ "data_store_only": False,
155
},
156
}
157
backend/app/connectors/velociraptor/services/artifacts.py
+26
-15
@@ -437,7 +437,7 @@ async def get_artifact_parameters_by_prefix_service(
437
438
async def run_artifact_collection(
439
collect_artifact_body: CollectArtifactBody,
440
- session: AsyncSession, # Add session parameter
440
+ session: AsyncSession,
441
) -> CollectArtifactResponse:
442
"""
443
Run an artifact collection on a client with optional parameters and upload results to MinIO.
@@ -558,14 +558,16 @@ async def run_artifact_collection(
558
completed = velociraptor_service.watch_flow_completion(flow_id, org_id=collect_artifact_body.velociraptor_org)
559
logger.info(f"Successfully watched flow completion on {completed}")
560
561
- results = velociraptor_service.read_collection_results(
562
- client_id=collect_artifact_body.velociraptor_id,
563
- flow_id=flow_id,
564
- org_id=collect_artifact_body.velociraptor_org,
565
- artifact=collect_artifact_body.artifact_name,
566
- )
567
-
568
- logger.info(f"Successfully read collection results on {results}")
561
+ # Only read collection results if data_store_only is False
562
+ results = None
563
+ if not collect_artifact_body.data_store_only:
564
+ results = velociraptor_service.read_collection_results(
565
+ client_id=collect_artifact_body.velociraptor_id,
566
+ flow_id=flow_id,
567
+ org_id=collect_artifact_body.velociraptor_org,
568
+ artifact=collect_artifact_body.artifact_name,
569
+ )
570
+ logger.info(f"Successfully read collection results on {results}")
571
572
# Fetch the collected file from filestore and upload to MinIO
573
file_data = None
@@ -603,12 +605,21 @@ async def run_artifact_collection(
605
logger.warning(f"Failed to upload file to MinIO, but artifact collection succeeded: {file_err}")
606
# Continue execution even if file upload fails
607
606
- return CollectArtifactResponse(
607
- success=results["success"],
608
- message=results["message"],
609
- results=results["results"],
610
- file_info=file_data if file_data and file_data.get("success") else None,
611
- )
608
+ # Build response based on data_store_only flag
609
+ if collect_artifact_body.data_store_only:
610
+ return CollectArtifactResponse(
611
+ success=True,
612
+ message="Artifact collected and stored successfully. Results not retrieved.",
613
+ results=None,
614
+ file_info=file_data if file_data and file_data.get("success") else None,
615
+ )
616
+ else:
617
+ return CollectArtifactResponse(
618
+ success=results["success"],
619
+ message=results["message"],
620
+ results=results["results"],
621
+ file_info=file_data if file_data and file_data.get("success") else None,
622
+ )
623
except HTTPException as he: # Catch HTTPException separately to propagate the original message
624
logger.error(
625
f"HTTPException while running artifact collection on {collect_artifact_body}: {he.detail}",
backend/app/version/services/version.py
+1
-1
@@ -7,7 +7,7 @@ from loguru import logger
7
from packaging.version import Version
8
9
# Current version - update this with each release
10
-CURRENT_VERSION = "0.1.29"
10
+CURRENT_VERSION = "0.1.30"
11
VERSION_CHECK_URL = "https://api.github.com/repos/socfortress/CoPilot/releases/latest"
12
13
frontend/src/api/endpoints/artifacts.ts
+70
-69
@@ -1,99 +1,100 @@
1
import type {
2
- Artifact,
3
- CollectResult,
4
- CommandResult,
5
- FileCollection,
6
- MatchingParameter,
7
- QuarantineResult,
8
- Recommendation
2
+ Artifact,
3
+ CollectResult,
4
+ CommandResult,
5
+ FileCollection,
6
+ MatchingParameter,
7
+ QuarantineResult,
8
+ Recommendation
9
} from "@/types/artifacts.d"
10
import type { OsTypesFull, OsTypesLower } from "@/types/common.d"
11
import type { FlaskBaseResponse } from "@/types/flask.d"
12
import { HttpClient } from "../httpClient"
13
14
export interface ArtifactsQuery {
15
- os?: OsTypesLower
16
- hostname?: string
15
+ os?: OsTypesLower
16
+ hostname?: string
17
}
18
19
export interface CollectRequest {
20
- hostname: string
21
- velociraptor_id?: string
22
- artifact_name: string
23
- parameters?: {
24
- env?: {
25
- key: string
26
- value: string
27
- }[]
28
- }
20
+ hostname: string
21
+ velociraptor_id?: string
22
+ artifact_name: string
23
+ data_store_only?: boolean
24
+ parameters?: {
25
+ env?: {
26
+ key: string
27
+ value: string
28
+ }[]
29
+ }
30
}
31
32
export interface CommandRequest {
32
- hostname: string
33
- velociraptor_id?: string
34
- command: string
35
- artifact_name: "Windows.System.PowerShell" | "Windows.System.CmdShell" | "Linux.Sys.BashShell"
33
+ hostname: string
34
+ velociraptor_id?: string
35
+ command: string
36
+ artifact_name: "Windows.System.PowerShell" | "Windows.System.CmdShell" | "Linux.Sys.BashShell"
37
}
38
39
export interface QuarantineRequest {
39
- hostname: string
40
- velociraptor_id?: string
41
- action: "quarantine" | "remove_quarantine"
42
- artifact_name: "Windows.Remediation.Quarantine" | "Linux.Remediation.Quarantine"
40
+ hostname: string
41
+ velociraptor_id?: string
42
+ action: "quarantine" | "remove_quarantine"
43
+ artifact_name: "Windows.Remediation.Quarantine" | "Linux.Remediation.Quarantine"
44
}
45
46
export interface ArtifactRecommendationRequest {
46
- os: OsTypesFull
47
- prompt: string | object
47
+ os: OsTypesFull
48
+ prompt: string | object
49
}
50
51
export interface FileCollectionByAgentRequest {
51
- file: string
52
- root_disk: string
52
+ file: string
53
+ root_disk: string
54
}
55
56
export default {
56
- getAll(filters?: ArtifactsQuery) {
57
- let url = "/artifacts"
57
+ getAll(filters?: ArtifactsQuery) {
58
+ let url = "/artifacts"
59
59
- if (filters?.os) {
60
- url = `/artifacts/${filters.os}`
61
- }
62
- if (filters?.hostname) {
63
- url = `/artifacts/hostname/${filters.hostname}`
64
- }
60
+ if (filters?.os) {
61
+ url = `/artifacts/${filters.os}`
62
+ }
63
+ if (filters?.hostname) {
64
+ url = `/artifacts/hostname/${filters.hostname}`
65
+ }
66
66
- return HttpClient.get<FlaskBaseResponse & { artifacts: Artifact[] }>(url)
67
- },
68
- getByName(artifactName: string) {
67
+ return HttpClient.get<FlaskBaseResponse & { artifacts: Artifact[] }>(url)
68
+ },
69
+ getByName(artifactName: string) {
70
return HttpClient.get<FlaskBaseResponse & { artifacts: Artifact[] }>(`/artifacts/artifact/${artifactName}`)
71
},
71
- collect(payload: CollectRequest) {
72
- return HttpClient.post<FlaskBaseResponse & { results: CollectResult[] }>(`/artifacts/collect`, payload)
73
- },
74
- command(payload: CommandRequest) {
75
- return HttpClient.post<FlaskBaseResponse & { results: CommandResult[] }>(`/artifacts/command`, payload)
76
- },
77
- quarantine(payload: QuarantineRequest) {
78
- return HttpClient.post<FlaskBaseResponse & { results: QuarantineResult[] }>(`/artifacts/quarantine`, payload)
79
- },
80
- getArtifactRecommendation(payload: ArtifactRecommendationRequest) {
81
- return HttpClient.post<FlaskBaseResponse & { recommendations: Recommendation[] }>(
82
- `/artifacts/velociraptor-artifact-recommendation`,
83
- payload
84
- )
85
- },
86
- getParameters(artifactName: string, parameterPrefix: string) {
87
- return HttpClient.get<
88
- FlaskBaseResponse & {
89
- artifact_name: string
90
- parameter_prefix: string
91
- matching_parameters: MatchingParameter[]
92
- total_matches: number
93
- }
94
- >(`/artifacts/artifact/${artifactName}/parameters/${parameterPrefix}`)
95
- },
96
- collectFileByAgentId(agentId: string, payload: FileCollectionByAgentRequest) {
97
- return HttpClient.post<FlaskBaseResponse & FileCollection>(`/artifacts/collect/file/agent/${agentId}`, payload)
98
- }
72
+ collect(payload: CollectRequest) {
73
+ return HttpClient.post<FlaskBaseResponse & { results: CollectResult[] }>(`/artifacts/collect`, payload)
74
+ },
75
+ command(payload: CommandRequest) {
76
+ return HttpClient.post<FlaskBaseResponse & { results: CommandResult[] }>(`/artifacts/command`, payload)
77
+ },
78
+ quarantine(payload: QuarantineRequest) {
79
+ return HttpClient.post<FlaskBaseResponse & { results: QuarantineResult[] }>(`/artifacts/quarantine`, payload)
80
+ },
81
+ getArtifactRecommendation(payload: ArtifactRecommendationRequest) {
82
+ return HttpClient.post<FlaskBaseResponse & { recommendations: Recommendation[] }>(
83
+ `/artifacts/velociraptor-artifact-recommendation`,
84
+ payload
85
+ )
86
+ },
87
+ getParameters(artifactName: string, parameterPrefix: string) {
88
+ return HttpClient.get<
89
+ FlaskBaseResponse & {
90
+ artifact_name: string
91
+ parameter_prefix: string
92
+ matching_parameters: MatchingParameter[]
93
+ total_matches: number
94
+ }
95
+ >(`/artifacts/artifact/${artifactName}/parameters/${parameterPrefix}`)
96
+ },
97
+ collectFileByAgentId(agentId: string, payload: FileCollectionByAgentRequest) {
98
+ return HttpClient.post<FlaskBaseResponse & FileCollection>(`/artifacts/collect/file/agent/${agentId}`, payload)
99
+ }
100
}
frontend/src/components/artifacts/ArtifactsCollect.vue
+35
-8
@@ -55,6 +55,19 @@
55
size="small"
56
/>
57
</div>
58
+ <div>
59
+ <n-tooltip trigger="hover" placement="top">
60
+ <template #trigger>
61
+ <n-checkbox v-model:checked="filters.data_store_only" :disabled="loading" size="small">
62
+ Store Only
63
+ </n-checkbox>
64
+ </template>
65
+ <div class="text-xs">
66
+ Skip rendering results in frontend and only store the artifact in the data store.<br />
67
+ This is faster for large collections.
68
+ </div>
69
+ </n-tooltip>
70
+ </div>
71
<div>
72
<n-button
73
size="small"
@@ -136,7 +149,12 @@
149
150
<n-spin :show="loading">
151
<div class="my-7 flex min-h-52 flex-col gap-3">
139
- <template v-if="collectList.length">
152
+ <template v-if="filters.data_store_only && isDirty">
153
+ <n-alert type="success" title="Artifact Stored Successfully" class="item-appear item-appear-bottom item-appear-005">
154
+ The artifact has been collected and stored in the data store. Results were not retrieved to improve performance.
155
+ </n-alert>
156
+ </template>
157
+ <template v-else-if="collectList.length">
158
<CollectItem
159
v-for="collect of collectList"
160
:key="`${collect.___id}`"
@@ -157,7 +175,7 @@
175
import type { ArtifactsQuery, CollectRequest } from "@/api/endpoints/artifacts"
176
import type { Agent } from "@/types/agents.d"
177
import type { Artifact, ArtifactParameter, CollectResult } from "@/types/artifacts.d"
160
-import { NButton, NCard, NEmpty, NInput, NPopover, NScrollbar, NSelect, NSpin, NTag, NTooltip, useMessage } from "naive-ui"
178
+import { NAlert, NButton, NCard, NCheckbox, NEmpty, NInput, NPopover, NScrollbar, NSelect, NSpin, NTag, NTooltip, useMessage } from "naive-ui"
179
import { nanoid } from "nanoid"
180
import { computed, nextTick, onBeforeMount, ref, toRefs } from "vue"
181
import Api from "@/api"
@@ -200,7 +218,9 @@ const total = computed<number>(() => {
218
return collectList.value.length || 0
219
})
220
203
-const filters = ref<Partial<CollectRequest>>({})
221
+const filters = ref<Partial<CollectRequest>>({
222
+ data_store_only: false
223
+})
224
225
const areFiltersValid = computed(() => {
226
return !!filters.value.artifact_name && !!filters.value.hostname
@@ -280,7 +300,8 @@ function getData() {
300
const payload: CollectRequest = {
301
...filters.value,
302
hostname: filters.value.hostname!,
283
- artifact_name: filters.value.artifact_name!
303
+ artifact_name: filters.value.artifact_name!,
304
+ data_store_only: filters.value.data_store_only || false
305
}
306
307
// Only add parameters if there are any
@@ -294,10 +315,16 @@ function getData() {
315
if (res.data.success) {
316
isDirty.value = true
317
297
- collectList.value = (res.data?.results || []).map(o => {
298
- o.___id = nanoid()
299
- return o
300
- })
318
+ // If data_store_only, results will be null/empty
319
+ if (filters.value.data_store_only) {
320
+ collectList.value = []
321
+ message.success(res.data?.message || "Artifact stored successfully")
322
+ } else {
323
+ collectList.value = (res.data?.results || []).map(o => {
324
+ o.___id = nanoid()
325
+ return o
326
+ })
327
+ }
328
} else {
329
message.warning(res.data?.message || "An error occurred. Please try again later.")
330
}