Skip to content

Commit

Permalink
Special case offline collector copy collection. (Velocidex#3577)
Browse files Browse the repository at this point in the history
The Offline Collector builder is a special GUI component used to build
an offline collector. It creates an instance of the
Server.Utils.CreateCollector artifact.

Previously when copying this collection, the default copy artifact
widget was used which allowed the user to modify the raw artifact
parameters to that artifact but this is not user friendly enough.

This PR opens the custom GUI component for editing allowing modification
of the offline collector in a more natural way.
  • Loading branch information
scudette authored Jun 20, 2024
1 parent 5f89b4c commit fe4489c
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 68 deletions.
2 changes: 1 addition & 1 deletion api/flows.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (self *ApiServer) GetClientFlows(
json.AnyToString(flow.Request.Creator, vjson.DefaultEncOpts()),
json.AnyToString(flow.TotalUploadedBytes, vjson.DefaultEncOpts()),
json.AnyToString(flow.TotalCollectedRows, vjson.DefaultEncOpts()),
json.MustMarshalString(flow),
json.MustMarshalProtobufString(flow, vjson.DefaultEncOpts()),
json.AnyToString(flow.Request.Urgent, vjson.DefaultEncOpts()),
json.AnyToString(flow.ArtifactsWithResults, vjson.DefaultEncOpts()),
}
Expand Down
14 changes: 10 additions & 4 deletions file_store/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"www.velocidex.com/golang/velociraptor/file_store/directory"
"www.velocidex.com/golang/velociraptor/file_store/memcache"
"www.velocidex.com/golang/velociraptor/file_store/memory"
"www.velocidex.com/golang/velociraptor/utils"
)

var (
Expand All @@ -44,7 +45,9 @@ func GetFileStore(config_obj *config_proto.Config) api.FileStore {
defer fs_mu.Unlock()

// Maintain a different filestore for each org.
impl, pres := g_impl[config_obj.OrgId]
org_id := utils.NormalizedOrgId(config_obj.OrgId)

impl, pres := g_impl[org_id]
if pres {
return impl
}
Expand All @@ -59,7 +62,7 @@ func GetFileStore(config_obj *config_proto.Config) api.FileStore {
}

res, _ := getImpl(implementation, config_obj)
g_impl[config_obj.OrgId] = res
g_impl[org_id] = res
return res
}

Expand Down Expand Up @@ -87,7 +90,8 @@ func OverrideFilestoreImplementation(
fs_mu.Lock()
defer fs_mu.Unlock()

g_impl[config_obj.OrgId] = impl
org_id := utils.NormalizedOrgId(config_obj.OrgId)
g_impl[org_id] = impl
}

func SetGlobalFilestore(
Expand All @@ -96,12 +100,14 @@ func SetGlobalFilestore(
fs_mu.Lock()
defer fs_mu.Unlock()

org_id := utils.NormalizedOrgId(config_obj.OrgId)

impl, err := getImpl(implementation, config_obj)
if err != nil {
return err
}

g_impl[config_obj.OrgId] = impl
g_impl[org_id] = impl
return nil
}

Expand Down
24 changes: 22 additions & 2 deletions gui/velociraptor/src/components/flows/flows-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,26 @@ class FlowsList extends React.Component {
this.props.collapseToggle(SLIDE_STATES[next_slide].level);
};

copyCollection = ()=> {
let selected_flows = this.props.selected_flow &&
this.props.selected_flow.artifacts_with_results;

// Special handling for offline collector
if (_.isEqual(selected_flows, ["Server.Utils.CreateCollector"])) {
let specs = this.props.selected_flow.request &&
this.props.selected_flow.request.specs;

if (!_.isArray(specs) || specs.length != 1) {
specs = [];
}

this.setState({
offlineSpecs: specs[0].parameters,
showOfflineWizard: true});
} else {
this.setState({showCopyWizard: true});
}
};

render() {
let tab = this.props.match && this.props.match.params &&
Expand Down Expand Up @@ -411,7 +430,7 @@ class FlowsList extends React.Component {
baseFlow={this.props.selected_flow}
onCancel={(e) => this.setState({showCopyWizard: false})}
onResolve={this.setCollectionRequest} />
}
}

{ this.state.showNewFromRouterWizard &&
<NewCollectionWizard
Expand All @@ -423,6 +442,7 @@ class FlowsList extends React.Component {

{ this.state.showOfflineWizard &&
<OfflineCollectorWizard
collector_parameters={this.state.offlineSpecs}
onCancel={(e) => this.setState({showOfflineWizard: false})}
onResolve={this.setCollectionRequest} />
}
Expand Down Expand Up @@ -483,7 +503,7 @@ class FlowsList extends React.Component {
<Button data-tooltip={T("Copy Collection")}
data-position="right"
className="btn-tooltip"
onClick={() => this.setState({showCopyWizard: true})}
onClick={this.copyCollection}
variant="default">
<FontAwesomeIcon icon="copy"/>
<span className="sr-only">{T("Copy Collection")}</span>
Expand Down
169 changes: 133 additions & 36 deletions gui/velociraptor/src/components/flows/offline-collector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import T from '../i8n/i8n.jsx';
import ToolViewer from "../tools/tool-viewer.jsx";
import ValidatedInteger from '../forms/validated_int.jsx';
import _ from 'lodash';
import api from '../core/api-service.jsx';
import {CancelToken} from 'axios';

import { JSONparse } from "../utils/json_parse.jsx";

// The offline collection wizard is built upon the new collection
// wizard with some extra steps.
Expand Down Expand Up @@ -720,53 +724,146 @@ class OfflineCollectionResources extends React.Component {
}
}

function getDefaultCollectionParameters() {
return {
target_os: "Windows",
target: "ZIP",
target_args: {
// Common
bucket: "",

// For GCS Buckets
GCSKey: "",

// For S3 buckets.
credentialsKey: "",
credentialsSecret: "",
credentialsToken: "",
region: "",
endpoint: "",
serverSideEncryption: "",
kmsEncryptionKey: "",
s3UploadRoot: "",

// For Azure
sas_url: "",
},
password: "",
pubkey: "",
encryption_scheme: "None",
encryption_args: {
public_key: "",
password: ""
},
opt_level: 5,
opt_output_directory: "",
opt_tempdir: "",
opt_filename_template: "Collection-%FQDN%-%TIMESTAMP%",
opt_format: "jsonl",
opt_prompt: "N",
};
}


export default class OfflineCollectorWizard extends React.Component {
static propTypes = {
collector_parameters: PropTypes.object,
onResolve: PropTypes.func,
onCancel: PropTypes.func,
};

componentDidMount = () => {
this.source = CancelToken.source();
if (this.props.collector_parameters) {

// This is basically the reverse of prepareRequest
let collector_parameters = this.state.collector_parameters;
let resources = {};
let env = this.props.collector_parameters &&
this.props.collector_parameters.env;

_.each(env, x=>{
if (_.isUndefined(x.value)) {
return;
}

switch(x.key) {
case "artifacts":
let artifact_list = JSONparse(x.value);
// Resolve the artifacts from the request into a
// list of descriptors.
api.post("v1/GetArtifacts", {names: artifact_list},
this.source.token).then(response=>{
if (response && response.data &&
response.data.items &&
response.data.items.length) {

this.setState({artifacts: [...response.data.items]});
}});

break;
case "OS":
collector_parameters.target_os = x.value;
break;
case "parameters":
this.setState({parameters: JSONparse(x.value)});
break;
case "target":
collector_parameters.target = x.value;
break;
case "target_args":
let target_args = JSONparse(x.value);
Object.assign(collector_parameters.target_args, target_args);
break;
case "encryption_scheme":
collector_parameters.encryption_scheme= x.value;
break;
case "encryption_args":
collector_parameters.encryption_args = JSONparse(x.value);
break;
case "opt_prompt":
collector_parameters.opt_prompt = x.value;
break;
case "opt_tempdir":
collector_parameters.opt_tempdir = x.value;
break;
case "opt_level":
collector_parameters.opt_level = x.value;
break;
case "opt_output_directory":
collector_parameters.opt_output_directory = x.value;
break;
case "opt_filename_template":
collector_parameters.opt_filename_template = x.value;
break;
case "opt_progress_timeout":
resources.progress_timeout = JSONparse(x.value);
break;
case "opt_timeout":
resources.timeout = JSONparse(x.value);
break;
case "opt_cpu_limit":
resources.cpu_limit = JSONparse(x.value);
break;
case "opt_format":
collector_parameters.opt_format = x.value;
break;
};
});
this.setResources(resources);
this.setState({collector_parameters: collector_parameters});
}
}

componentWillUnmount() {
this.source.cancel("unmounted");
}


state = {
artifacts: [],
parameters: {},
collector_parameters: {
target_os: "Windows",
target: "ZIP",
target_args: {
// Common
bucket: "",

// For GCS Buckets
GCSKey: "",

// For S3 buckets.
credentialsKey: "",
credentialsSecret: "",
credentialsToken: "",
region: "",
endpoint: "",
serverSideEncryption: "",
kmsEncryptionKey: "",
s3UploadRoot: "",

// For Azure
sas_url: "",
},
password: "",
pubkey: "",
encryption_scheme: "None",
encryption_args: {
public_key: "",
password: ""
},
opt_level: 5,
opt_output_directory: "",
opt_filename_template: "Collection-%FQDN%-%TIMESTAMP%",
opt_format: "jsonl",
opt_prompt: "N",
},
collector_parameters: getDefaultCollectionParameters(),
resources: {},
}

Expand Down
5 changes: 5 additions & 0 deletions json/protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
)

func MustMarshalProtobufString(v interface{}, opts *json.EncOpts) string {
res, _ := MarshalProtobuf(v, opts)
return string(res)
}

func MarshalProtobuf(v interface{}, opts *json.EncOpts) ([]byte, error) {
self, ok := v.(proto.Message)
if !ok {
Expand Down
2 changes: 1 addition & 1 deletion services/frontend/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func NewFrontendService(ctx context.Context, wg *sync.WaitGroup,
}

// Sub orgs just use the same frontend manager
if config_obj.OrgId != "" {
if !utils.IsRootOrg(config_obj.OrgId) {
org_manager, err := services.GetOrgManager()
if err != nil {
return nil, err
Expand Down
7 changes: 5 additions & 2 deletions services/hunt_manager/hunt_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ func (self *HuntManager) maybeDirectlyAssignFlow(
Set("ClientId", assignment.ClientId).
Set("FlowId", assignment.FlowId).
Set("Timestamp", utils.GetTime().Now().Unix()),
}, services.JournalOptions{
Sync: true,
})
if err != nil {
return err
Expand Down Expand Up @@ -425,7 +427,7 @@ func (self *HuntManager) ProcessFlowCompletion(
Set("StartTime", time.Unix(0, int64(flow.StartTime*1000))).
Set("EndTime", time.Unix(0, int64(flow.ActiveTime*1000))).
Set("Status", flow.State.String()).
Set("Error", flow.Status)})
Set("Error", flow.Status)}, services.JournalOptions{})
}

// When a label is changed we check all the active hunts to see if any
Expand Down Expand Up @@ -785,7 +787,8 @@ func scheduleHuntOnClient(

path_manager := paths.NewHuntPathManager(hunt_id)
err = journal.AppendToResultSet(config_obj,
path_manager.Clients(), []*ordereddict.Dict{row})
path_manager.Clients(), []*ordereddict.Dict{row},
services.JournalOptions{})
if err != nil {
return err
}
Expand Down
7 changes: 6 additions & 1 deletion services/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"www.velocidex.com/golang/velociraptor/file_store/api"
)

type JournalOptions struct {
Sync bool
}

func GetJournal(config_obj *config_proto.Config) (JournalService, error) {
org_manager, err := GetOrgManager()
if err != nil {
Expand All @@ -48,7 +52,8 @@ type JournalService interface {
// Push the rows into the result set in the filestore. NOTE: This
// method synchronises access to the files within the process.
AppendToResultSet(config_obj *config_proto.Config,
path api.FSPathSpec, rows []*ordereddict.Dict) error
path api.FSPathSpec, rows []*ordereddict.Dict,
options JournalOptions) error

Broadcast(ctx context.Context, config_obj *config_proto.Config,
rows []*ordereddict.Dict, name, client_id, flows_id string) error
Expand Down
Loading

0 comments on commit fe4489c

Please sign in to comment.