@cryptotaxi247 / CoPilot / commits / 35a8b35a

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 }