diff --git a/artifacts/definitions/Generic/Forensic/HashLookup.yaml b/artifacts/definitions/Generic/Forensic/HashLookup.yaml new file mode 100644 index 00000000000..2937cf32758 --- /dev/null +++ b/artifacts/definitions/Generic/Forensic/HashLookup.yaml @@ -0,0 +1,38 @@ +name: Generic.Forensic.HashLookup +description: | + This artifact is a server event artifact that collects hashes from + various sources into a central location. It is possible to follow + this artifact (e.g. with an external program using the API) to + lookup the hashes with an external service. + + You can also send hashes to this artifact yourself using the + `send_event()` vql Function. For example, the following will add + hashes from the results of another artifact. + + ```vql + SELECT *, send_event( + artifact="Generic.Forensic.HashLookup", + row=dict(SHA256=Sha256, ClientId=ClientId)) + FROM source() + ``` + +type: SERVER_EVENT + +sources: + - query: | + // You can add more queries to this chain to automatically + // collect more hashes. + SELECT ClientId, SHA256 FROM chain( + a={ + SELECT * FROM foreach( + row={ + SELECT ClientId, FlowId + FROM watch_monitoring(artifact="System.Flow.Completion") + WHERE Flow.artifacts_with_results =~ "System.VFS.DownloadFile" + }, query={ + SELECT ClientId, Sha256 AS SHA256 + FROM source( + artifact="System.VFS.DownloadFile", + client_id=ClientId, flow_id=FlowId) + }) + }, async=TRUE) diff --git a/bin/frontend.go b/bin/frontend.go index 7ea869fb598..dc1c994f1b0 100644 --- a/bin/frontend.go +++ b/bin/frontend.go @@ -37,8 +37,18 @@ var ( frontend_cmd_minion = frontend_cmd.Flag("minion", "This is a minion frontend").Bool() frontend_cmd_node = frontend_cmd.Flag("node", "The name of a minion - selects from available frontend configurations").String() + + frontend_disable_panic_guard = frontend_cmd.Flag("disable-panic-guard", + "Disabled the panic guard mechanism (not recommended)").Bool() ) +func doFrontendWithPanicGuard() { + if !*frontend_disable_panic_guard { + writeLogOnPanic() + } + doFrontend() +} + func doFrontend() { config_obj, err := makeDefaultConfigLoader(). WithRequiredFrontend(). @@ -133,7 +143,7 @@ func startFrontend(sm *services.Service) (*api.Builder, error) { func init() { command_handlers = append(command_handlers, func(command string) bool { if command == frontend_cmd.FullCommand() { - doFrontend() + doFrontendWithPanicGuard() return true } return false diff --git a/bin/panic.go b/bin/panic.go new file mode 100644 index 00000000000..cb5717549f5 --- /dev/null +++ b/bin/panic.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/mitchellh/panicwrap" + kingpin "gopkg.in/alecthomas/kingpin.v2" + "www.velocidex.com/golang/velociraptor/config" +) + +func writeLogOnPanic() { + // Figure out the log directory. + config_obj, err := new(config.Loader). + WithFileLoader(*config_path). + WithEmbedded(). + WithEnvLoader("VELOCIRAPTOR_CONFIG"). + LoadAndValidate() + kingpin.FatalIfError(err, "Unable to load config file") + + if config_obj.Logging != nil && + config_obj.Logging.OutputDirectory != "" { + exitStatus, err := panicwrap.BasicWrap(func(output string) { + // Create a special log file in the log directory. + filename := filepath.Join( + config_obj.Logging.OutputDirectory, + fmt.Sprintf("panic-%v.log", strings.Replace(":", "_", + time.Now().Format(time.RFC3339), -1))) + + fd, err := os.OpenFile(filename, + os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return + } + fd.Write([]byte(output)) + fd.Close() + }) + if err != nil { + // Something went wrong setting up the panic + // wrapper. Unlikely, but possible. + panic(err) + } + + // If exitStatus >= 0, then we're the parent process + // and the panicwrap re-executed ourselves and + // completed. Just exit with the proper status. + if exitStatus >= 0 { + os.Exit(exitStatus) + } + + // Otherwise, exitStatus < 0 means we're the + // child. Continue executing as normal... + } +} diff --git a/docs/references/vql.yaml b/docs/references/vql.yaml index 66212b6a3e8..47c79676e46 100644 --- a/docs/references/vql.yaml +++ b/docs/references/vql.yaml @@ -2851,6 +2851,22 @@ type: string description: The type of search (e.g. 'key') category: server +- name: send_event + description: | + Sends an event to a server event monitoring queue. + + This is used to send an event to a waiting server event monitoring + artifact (either as a VQL query running on the server or perhaps + an external program waiting for this event via the API. + + type: Function + args: + - name: artifact + type: string + required: true + - name: row + type: ordereddict.Dict + required: true - name: serialize description: Encode an object as a string (csv or json). type: Function diff --git a/go.mod b/go.mod index bbc22ea4673..e5fd9032498 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.6 github.com/microcosm-cc/bluemonday v1.0.15 github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/panicwrap v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.4 github.com/oschwald/maxminddb-golang v1.8.0 // indirect github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index f0df6a7fc12..c1b4ee9f439 100644 --- a/go.sum +++ b/go.sum @@ -425,6 +425,8 @@ github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSO github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= +github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/gui/velociraptor/src/components/flows/new-collection.js b/gui/velociraptor/src/components/flows/new-collection.js index 7f4ee941a56..1bfe88b4089 100644 --- a/gui/velociraptor/src/components/flows/new-collection.js +++ b/gui/velociraptor/src/components/flows/new-collection.js @@ -30,7 +30,7 @@ import { HotKeys, ObserveKeys } from "react-hotkeys"; import { requestToParameters } from "./utils.js"; import api from '../core/api-service.js'; - +import axios from 'axios'; class PaginationBuilder { PaginationSteps = ["Select Artifacts", "Configure Parameters", @@ -103,7 +103,8 @@ class NewCollectionSelectArtifacts extends React.Component { artifacts: PropTypes.array, // Update the wizard's artifacts list. - setArtifacts: PropTypes.func, + setArtifacts: PropTypes.func.isRequired, + setParameters: PropTypes.func.isRequired, paginator: PropTypes.object, // Artifact type CLIENT, SERVER, CLIENT_EVENT, SERVER_EVENT @@ -126,6 +127,7 @@ class NewCollectionSelectArtifacts extends React.Component { } componentDidMount = () => { + this.source = axios.CancelToken.source(); this.doSearch("..."); const el = document.getElementById("text-filter-column-name-search-for-artifact-input"); if (el) { @@ -133,6 +135,10 @@ class NewCollectionSelectArtifacts extends React.Component { }; } + componentWillUnmount() { + this.source.cancel(); + } + onSelect = (row, isSelect) => { let new_artifacts = []; if (isSelect) { @@ -142,7 +148,6 @@ class NewCollectionSelectArtifacts extends React.Component { } this.props.setArtifacts(new_artifacts); this.setState({selectedDescriptor: row}); - } onSelectAll = (isSelect, rows) => { @@ -194,6 +199,35 @@ class NewCollectionSelectArtifacts extends React.Component { }); } + setFavorite = (spec) => { + // First set all the artifacts in the spec. + let artifacts = []; + _.each(spec, x=>artifacts.push(x.artifact)); + + api.get("v1/GetArtifacts", { + type: this.props.artifactType, + names: artifacts}).then((response) => { + let items = response.data.items || []; + this.setState({ + selectedDescriptor: items[0], + matchingDescriptors: items, + }); + // Set the artifacts. + this.props.setArtifacts(items); + this.props.setParameters(requestToParameters({specs: spec})); + + }, this.source.token); + } + + setFavorite_ = (selection) => { + _.each(this.state.favorites, fav=>{ + if(selection.value === fav.name) { + this.setFavorite(fav.spec); + } + }); + return true; + } + render() { let columns = [{dataField: "name", text: "", formatter: (cell, row) => { @@ -224,7 +258,7 @@ class NewCollectionSelectArtifacts extends React.Component { className="favorites" classNamePrefix="velo" options={this.state.favorite_options} - onChange={this.setFavorite} + onChange={this.setFavorite_} placeholder="Favorite Name" />}