From df206814a1a6848ef3b6fd4beb700bfe8aac318e Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Sun, 4 Jan 2026 13:06:32 +1100 Subject: [PATCH 01/24] feat: add verify command JSON report --- bin/verify.go | 43 +++++ services/launcher/verifier_report.go | 231 +++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 services/launcher/verifier_report.go diff --git a/bin/verify.go b/bin/verify.go index cb9dc424b55..0c4ca54ee91 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "time" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" "www.velocidex.com/golang/velociraptor/constants" @@ -17,6 +18,9 @@ var ( verify = artifact_command.Command("verify", "Verify a set of artifacts") verify_args = verify.Arg("paths", "Paths to artifact yaml files").Required().Strings() verify_allow_override = verify.Flag("builtin", "Allow overriding of built in artifacts").Bool() + verify_soft_fail = verify.Flag("soft_fail", "Do not return error code on verification failures").Bool() + verify_format = verify.Flag("format", "Output format (json)").Default("").String() + verify_output = verify.Flag("output", "Output file for report").Default("").String() ) func doVerify() error { @@ -104,6 +108,45 @@ func doVerify() error { } } + if *verify_soft_fail { + ret = nil + } + + if *verify_format != "" { + report, err := launcher.NewVerifierReporter(*verify_format) + if err != nil { + logger.Error("verifier: %v", err) + return ret + } + + for artifact_path, state := range states { + name := "Unknown" + if artifact, ok := artifacts[artifact_path]; ok { + name = artifact.GetName() + } + + report.AddArtifact(name, artifact_path, state) + } + + outfile := *verify_output + + if outfile == "" { + outfile = fmt.Sprintf("report_%d.%s", time.Now().Unix(), *verify_format) + } + + file, err := os.Create(outfile) + if err != nil { + logger.Error("verifier: %v", err) + return ret + } + defer file.Close() + + report.SetExit(ret) + report.Generate(file) + + logger.Info("verifier: wrote %s report to '%s'", *verify_format, file.Name()) + } + return ret } diff --git a/services/launcher/verifier_report.go b/services/launcher/verifier_report.go new file mode 100644 index 00000000000..9d8d8a90b01 --- /dev/null +++ b/services/launcher/verifier_report.go @@ -0,0 +1,231 @@ +/* +Supports the generation of text-based reports for the artifacts verify command. + +This file provides an interface and implementation for creating verification reports. +It currently only supports the JSON format, but is designed to be extensible for future formats. +*/ + +package launcher + +import ( + "encoding/json" + "fmt" + "io" + "os" + "slices" + "sort" + "strings" + "time" + + "www.velocidex.com/golang/velociraptor/config" +) + +// VerifierReporter is the base interface for generating verification reports. +type VerifierReporter interface { + AddArtifact(artifactName string, artifactPath string, state *AnalysisState) + SetExit(err error) + Generate(w io.Writer) error +} + +// NewVerifierReporter creates a new VerifierReporter based on the specified format. +func NewVerifierReporter(format string) (VerifierReporter, error) { + switch format { + case "json": + return &JsonVerifierReporter{}, nil + default: + return nil, fmt.Errorf("unknown report format '%v'", format) + } +} + +/* + JSON Verification Report +*/ + +// JsonVerifierReporter generates a JSON formatted verification report. +type JsonVerifierReporter struct { + artifacts []*JsonVerifierState + exitCode int +} + +// AddArtifact adds an artifact and its analysis state to the report. +func (r *JsonVerifierReporter) AddArtifact(artifactName string, artifactPath string, state *AnalysisState) { + r.artifacts = append(r.artifacts, &JsonVerifierState{ + Name: artifactName, + Path: artifactPath, + State: state, + }) +} + +// Generate writes the JSON report to the provided writer. +func (r *JsonVerifierReporter) Generate(w io.Writer) error { + version := config.GetVersion() + + report := &JsonVerifierFormat{ + Timestamp: time.Now().UTC().Format(time.RFC3339), + ExitCode: r.exitCode, + Metadata: &VerifierMetadata{ + Version: version.Version, + Commit: version.Commit, + BuildTime: version.BuildTime, + Command: strings.Join(os.Args, " "), + }, + Summary: r.populateSummary(), + Artifacts: r.populateArtifacts(), + Results: r.populateResults(), + } + + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + + return encoder.Encode(report) +} + +// SetExit sets the exit code value for the report. +func (r *JsonVerifierReporter) SetExit(err error) { + if err != nil { + r.exitCode = 1 + } else { + r.exitCode = 0 + } +} + +// JsonVerifierState represents the verification state of a single artifact. +type JsonVerifierState struct { + Name string + Path string + State *AnalysisState +} + +// FormatResult formats the verification result for JSON output. +func (r *JsonVerifierState) FormatResult() *JsonVerifierResult { + result := &JsonVerifierResult{ + Name: r.Name, + Path: r.Path, + Status: string(r.getStatus()), + Errors: make([]string, 0, len(r.State.Errors)), + Warnings: make([]string, 0, len(r.State.Warnings)), + } + + for _, err := range r.State.Errors { + result.Errors = append(result.Errors, err.Error()) + } + + for _, warning := range r.State.Warnings { + result.Warnings = append(result.Warnings, warning) + } + + return result +} + +// resultStatus represents the status of a verification result. +type resultStatus string + +const ( + StatusPass resultStatus = "pass" + StatusWarning resultStatus = "warning" + StatusFail resultStatus = "fail" +) + +// getStatus determines the overall status of the artifact based on its errors and warnings. +func (r *JsonVerifierState) getStatus() resultStatus { + if len(r.State.Errors) > 0 { + return StatusFail + } + + if len(r.State.Warnings) > 0 { + return StatusWarning + } + + return StatusPass +} + +// JsonVerifierFormat represents the overall JSON report structure. +type JsonVerifierFormat struct { + Timestamp string `json:"timestamp"` + Metadata *VerifierMetadata `json:"metadata"` + ExitCode int `json:"exit_code"` + Summary *JsonVerifierSummary `json:"summary"` + Artifacts []string `json:"artifacts"` + Results []*JsonVerifierResult `json:"results"` +} + +// VerifierMetadata contains metadata about the verification run. +type VerifierMetadata struct { + Version string `json:"version"` + Commit string `json:"commit"` + BuildTime string `json:"build_time"` + Command string `json:"command"` +} + +// JsonVerifierSummary provides a summary of the verification results. +type JsonVerifierSummary struct { + Total int `json:"total"` + Passed int `json:"passed"` + Warning int `json:"warning"` + Failed int `json:"failed"` +} + +// JsonVerifierResult represents the verification result for a single artifact. +type JsonVerifierResult struct { + Name string `json:"name"` + Path string `json:"path"` + Status string `json:"status"` + Errors []string `json:"errors"` + Warnings []string `json:"warnings"` +} + +// populateSummary compiles the summary of verification results. +func (r *JsonVerifierReporter) populateSummary() *JsonVerifierSummary { + summary := &JsonVerifierSummary{} + summary.Total = len(r.artifacts) + + for _, artifact := range r.artifacts { + status := artifact.getStatus() + switch status { + case StatusPass: + summary.Passed++ + case StatusWarning: + summary.Warning++ + case StatusFail: + summary.Failed++ + } + } + + return summary +} + +// populateArtifacts compiles a list of artifact names included in the report, sorted alphabetically. +func (r *JsonVerifierReporter) populateArtifacts() []string { + artifacts := make([]string, 0, len(r.artifacts)) + seen := make(map[string]struct{}, len(r.artifacts)) + + for _, artifact := range r.artifacts { + if _, ok := seen[artifact.Name]; ok { + continue + } + + seen[artifact.Name] = struct{}{} + artifacts = append(artifacts, artifact.Name) + } + + sorted := slices.Clone(artifacts) + sort.Strings(sorted) + + return sorted +} + +// populateResults compiles the results for all artifacts, sorted by artifact path. +func (r *JsonVerifierReporter) populateResults() []*JsonVerifierResult { + results := make([]*JsonVerifierResult, 0, len(r.artifacts)) + + sortedArtifacts := slices.Clone(r.artifacts) + slices.SortFunc(sortedArtifacts, func(a, b *JsonVerifierState) int { + return strings.Compare(a.Path, b.Path) + }) + + for _, artifact := range sortedArtifacts { + results = append(results, artifact.FormatResult()) + } + + return results +} From c068283e749fc05ee8a7154a02714c5d8cc50a06 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Sun, 4 Jan 2026 19:30:31 +1100 Subject: [PATCH 02/24] test: add verify JSON report tests --- services/launcher/verifier_report_test.go | 178 ++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 services/launcher/verifier_report_test.go diff --git a/services/launcher/verifier_report_test.go b/services/launcher/verifier_report_test.go new file mode 100644 index 00000000000..4aed9393833 --- /dev/null +++ b/services/launcher/verifier_report_test.go @@ -0,0 +1,178 @@ +package launcher + +import ( + "bytes" + "fmt" + "slices" + "testing" + + "www.velocidex.com/golang/velociraptor/json" +) + +// Helper function to create an AnalysisState for testing. +func state(artifact string, errors []error, warnings []string) *AnalysisState { + return &AnalysisState{ + Artifact: artifact, + Permissions: nil, + Errors: errors, + Warnings: warnings, + } +} + +func TestNewVerifierReporter(t *testing.T) { + reporter, err := NewVerifierReporter("json") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if _, ok := reporter.(*JsonVerifierReporter); !ok { + t.Fatalf("expected JsonVerifierReporter, got %T", reporter) + } + + _, err = NewVerifierReporter("unknown") + if err == nil { + t.Fatalf("expected error for unknown format, got nil") + } +} + +func TestJsonVerifierStateGetStatus(t *testing.T) { + tests := []struct { + name string + state *AnalysisState + expected resultStatus + }{ + { + name: "pass", + state: state("Artifact1", nil, nil), + expected: StatusPass, + }, + { + name: "warning", + state: state("Artifact2", nil, []string{"warning"}), + expected: StatusWarning, + }, + { + name: "fail", + state: state("Artifact3", []error{fmt.Errorf("error")}, nil), + expected: StatusFail, + }, + { + name: "fail_with_warning", + state: state("Artifact4", []error{fmt.Errorf("error")}, []string{"warning"}), + expected: StatusFail, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonState := &JsonVerifierState{ + Name: tt.state.Artifact, + State: tt.state, + } + if status := jsonState.getStatus(); status != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, status) + } + }) + } +} + +func TestJsonVerifierStateFormatResult(t *testing.T) { + js := &JsonVerifierState{ + Name: "Artifact1", + Path: "./Artifact1.yaml", + State: state("Artifact1", []error{fmt.Errorf("error1"), fmt.Errorf("error2")}, []string{"warning1", "warning2"}), + } + + result := js.FormatResult() + + if result.Name != "Artifact1" { + t.Errorf("expected name 'Artifact1', got '%s'", result.Name) + } + + if result.Path != "./Artifact1.yaml" { + t.Errorf("expected path './Artifact1.yaml', got '%s'", result.Path) + } + + if result.Status != string(StatusFail) { + t.Errorf("expected status 'fail', got '%s'", result.Status) + } + + if len(result.Errors) != 2 || result.Errors[0] != "error1" || result.Errors[1] != "error2" { + t.Errorf("unexpected errors: %v", result.Errors) + } + + if len(result.Warnings) != 2 || result.Warnings[0] != "warning1" || result.Warnings[1] != "warning2" { + t.Errorf("unexpected warnings: %v", result.Warnings) + } +} + +func TestJsonVerifierPopulateSummary(t *testing.T) { + report := &JsonVerifierReporter{} + report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) + report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", []error{fmt.Errorf("error")}, nil)) + report.AddArtifact("Artifact3", "./Artifact3.yaml", state("Artifact3", nil, []string{"warning"})) + report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", []error{fmt.Errorf("error")}, []string{"warning"})) + + result := report.populateSummary() + + if result.Total != 4 || result.Passed != 1 || result.Warning != 1 || result.Failed != 2 { + t.Errorf("unexpected summary: %+v", result) + } +} + +func TestJsonVerifierPopulateArtifacts(t *testing.T) { + report := &JsonVerifierReporter{} + report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", nil, nil)) + report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) + report.AddArtifact("Artifact3", "./Artifact3.yaml", state("Artifact3", nil, nil)) + + artifacts := report.populateArtifacts() + expected := []string{"Artifact1", "Artifact2", "Artifact3"} + + if !slices.Equal(artifacts, expected) { + t.Errorf("expected %v, got %v", expected, artifacts) + } +} +func TestJsonVerifierPopulateResults(t *testing.T) { + report := &JsonVerifierReporter{} + report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) + report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", nil, nil)) + + result := report.populateResults() + + if len(result) != 2 { + t.Fatalf("expected 2 results, got %d", len(result)) + } + + if result[0].Path != "./Artifact1.yaml" || result[1].Path != "./Artifact2.yaml" { + t.Errorf("results not sorted by path: %v", result) + } +} + +func TestJsonVerifierGenerate(t *testing.T) { + report := &JsonVerifierReporter{} + report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) + report.SetExit(nil) + + var buf bytes.Buffer + if err := report.Generate(&buf); err != nil { + t.Fatalf("generate failed: %v", err) + } + + var result JsonVerifierFormat + if err := json.Unmarshal(buf.Bytes(), &result); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + if result.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", result.ExitCode) + } + + if result.Summary.Passed != 1 { + t.Errorf("expected 1 passed, got %d", result.Summary.Passed) + } + + if len(result.Results) != 1 || result.Results[0].Status != "pass" { + t.Errorf("unexpected results: %v", result.Results) + } +} From 0a3f7607adf0b2a571d584f187e511f4954705fe Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Sun, 4 Jan 2026 20:25:13 +1100 Subject: [PATCH 03/24] fix: make format case-insensitive --- bin/verify.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/verify.go b/bin/verify.go index 0c4ca54ee91..45a1dcd6514 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "time" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" @@ -113,7 +114,8 @@ func doVerify() error { } if *verify_format != "" { - report, err := launcher.NewVerifierReporter(*verify_format) + format := strings.ToLower(*verify_format) + report, err := launcher.NewVerifierReporter(format) if err != nil { logger.Error("verifier: %v", err) return ret @@ -131,7 +133,7 @@ func doVerify() error { outfile := *verify_output if outfile == "" { - outfile = fmt.Sprintf("report_%d.%s", time.Now().Unix(), *verify_format) + outfile = fmt.Sprintf("report_%d.%s", time.Now().Unix(), format) } file, err := os.Create(outfile) @@ -144,7 +146,7 @@ func doVerify() error { report.SetExit(ret) report.Generate(file) - logger.Info("verifier: wrote %s report to '%s'", *verify_format, file.Name()) + logger.Info("verifier: wrote %s report to '%s'", format, file.Name()) } return ret From c370bb289e3b09630237cec8fb703aebf1d2cf4f Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Sun, 4 Jan 2026 20:34:56 +1100 Subject: [PATCH 04/24] fix: create output path if it doesn't exist --- bin/verify.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/verify.go b/bin/verify.go index 45a1dcd6514..c1951c132e1 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "strings" "time" @@ -136,6 +137,13 @@ func doVerify() error { outfile = fmt.Sprintf("report_%d.%s", time.Now().Unix(), format) } + dir := filepath.Dir(outfile) + err = os.MkdirAll(dir, 0755) + if err != nil { + logger.Error("verifier: %v", err) + return ret + } + file, err := os.Create(outfile) if err != nil { logger.Error("verifier: %v", err) From 4cf7e1e00c77f80da3cacc228b8a30355b36532c Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Mon, 5 Jan 2026 14:08:41 +1100 Subject: [PATCH 05/24] revert: json report feature --- bin/verify.go | 53 ----- services/launcher/verifier_report.go | 231 ---------------------- services/launcher/verifier_report_test.go | 178 ----------------- 3 files changed, 462 deletions(-) delete mode 100644 services/launcher/verifier_report.go delete mode 100644 services/launcher/verifier_report_test.go diff --git a/bin/verify.go b/bin/verify.go index c9e83d35ffa..b424cdb4121 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -3,9 +3,6 @@ package main import ( "fmt" "os" - "path/filepath" - "strings" - "time" errors "github.com/go-errors/errors" artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" @@ -21,9 +18,6 @@ var ( verify = artifact_command.Command("verify", "Verify a set of artifacts") verify_args = verify.Arg("paths", "Paths to artifact yaml files").Required().Strings() verify_allow_override = verify.Flag("builtin", "Allow overriding of built in artifacts").Bool() - verify_soft_fail = verify.Flag("soft_fail", "Do not return error code on verification failures").Bool() - verify_format = verify.Flag("format", "Output format (json)").Default("").String() - verify_output = verify.Flag("output", "Output file for report").Default("").String() ) func doVerify() error { @@ -111,53 +105,6 @@ func doVerify() error { } } - if *verify_soft_fail { - ret = nil - } - - if *verify_format != "" { - format := strings.ToLower(*verify_format) - report, err := launcher.NewVerifierReporter(format) - if err != nil { - logger.Error("verifier: %v", err) - return ret - } - - for artifact_path, state := range states { - name := "Unknown" - if artifact, ok := artifacts[artifact_path]; ok { - name = artifact.GetName() - } - - report.AddArtifact(name, artifact_path, state) - } - - outfile := *verify_output - - if outfile == "" { - outfile = fmt.Sprintf("report_%d.%s", time.Now().Unix(), format) - } - - dir := filepath.Dir(outfile) - err = os.MkdirAll(dir, 0755) - if err != nil { - logger.Error("verifier: %v", err) - return ret - } - - file, err := os.Create(outfile) - if err != nil { - logger.Error("verifier: %v", err) - return ret - } - defer file.Close() - - report.SetExit(ret) - report.Generate(file) - - logger.Info("verifier: wrote %s report to '%s'", format, file.Name()) - } - return ret } diff --git a/services/launcher/verifier_report.go b/services/launcher/verifier_report.go deleted file mode 100644 index 9d8d8a90b01..00000000000 --- a/services/launcher/verifier_report.go +++ /dev/null @@ -1,231 +0,0 @@ -/* -Supports the generation of text-based reports for the artifacts verify command. - -This file provides an interface and implementation for creating verification reports. -It currently only supports the JSON format, but is designed to be extensible for future formats. -*/ - -package launcher - -import ( - "encoding/json" - "fmt" - "io" - "os" - "slices" - "sort" - "strings" - "time" - - "www.velocidex.com/golang/velociraptor/config" -) - -// VerifierReporter is the base interface for generating verification reports. -type VerifierReporter interface { - AddArtifact(artifactName string, artifactPath string, state *AnalysisState) - SetExit(err error) - Generate(w io.Writer) error -} - -// NewVerifierReporter creates a new VerifierReporter based on the specified format. -func NewVerifierReporter(format string) (VerifierReporter, error) { - switch format { - case "json": - return &JsonVerifierReporter{}, nil - default: - return nil, fmt.Errorf("unknown report format '%v'", format) - } -} - -/* - JSON Verification Report -*/ - -// JsonVerifierReporter generates a JSON formatted verification report. -type JsonVerifierReporter struct { - artifacts []*JsonVerifierState - exitCode int -} - -// AddArtifact adds an artifact and its analysis state to the report. -func (r *JsonVerifierReporter) AddArtifact(artifactName string, artifactPath string, state *AnalysisState) { - r.artifacts = append(r.artifacts, &JsonVerifierState{ - Name: artifactName, - Path: artifactPath, - State: state, - }) -} - -// Generate writes the JSON report to the provided writer. -func (r *JsonVerifierReporter) Generate(w io.Writer) error { - version := config.GetVersion() - - report := &JsonVerifierFormat{ - Timestamp: time.Now().UTC().Format(time.RFC3339), - ExitCode: r.exitCode, - Metadata: &VerifierMetadata{ - Version: version.Version, - Commit: version.Commit, - BuildTime: version.BuildTime, - Command: strings.Join(os.Args, " "), - }, - Summary: r.populateSummary(), - Artifacts: r.populateArtifacts(), - Results: r.populateResults(), - } - - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - - return encoder.Encode(report) -} - -// SetExit sets the exit code value for the report. -func (r *JsonVerifierReporter) SetExit(err error) { - if err != nil { - r.exitCode = 1 - } else { - r.exitCode = 0 - } -} - -// JsonVerifierState represents the verification state of a single artifact. -type JsonVerifierState struct { - Name string - Path string - State *AnalysisState -} - -// FormatResult formats the verification result for JSON output. -func (r *JsonVerifierState) FormatResult() *JsonVerifierResult { - result := &JsonVerifierResult{ - Name: r.Name, - Path: r.Path, - Status: string(r.getStatus()), - Errors: make([]string, 0, len(r.State.Errors)), - Warnings: make([]string, 0, len(r.State.Warnings)), - } - - for _, err := range r.State.Errors { - result.Errors = append(result.Errors, err.Error()) - } - - for _, warning := range r.State.Warnings { - result.Warnings = append(result.Warnings, warning) - } - - return result -} - -// resultStatus represents the status of a verification result. -type resultStatus string - -const ( - StatusPass resultStatus = "pass" - StatusWarning resultStatus = "warning" - StatusFail resultStatus = "fail" -) - -// getStatus determines the overall status of the artifact based on its errors and warnings. -func (r *JsonVerifierState) getStatus() resultStatus { - if len(r.State.Errors) > 0 { - return StatusFail - } - - if len(r.State.Warnings) > 0 { - return StatusWarning - } - - return StatusPass -} - -// JsonVerifierFormat represents the overall JSON report structure. -type JsonVerifierFormat struct { - Timestamp string `json:"timestamp"` - Metadata *VerifierMetadata `json:"metadata"` - ExitCode int `json:"exit_code"` - Summary *JsonVerifierSummary `json:"summary"` - Artifacts []string `json:"artifacts"` - Results []*JsonVerifierResult `json:"results"` -} - -// VerifierMetadata contains metadata about the verification run. -type VerifierMetadata struct { - Version string `json:"version"` - Commit string `json:"commit"` - BuildTime string `json:"build_time"` - Command string `json:"command"` -} - -// JsonVerifierSummary provides a summary of the verification results. -type JsonVerifierSummary struct { - Total int `json:"total"` - Passed int `json:"passed"` - Warning int `json:"warning"` - Failed int `json:"failed"` -} - -// JsonVerifierResult represents the verification result for a single artifact. -type JsonVerifierResult struct { - Name string `json:"name"` - Path string `json:"path"` - Status string `json:"status"` - Errors []string `json:"errors"` - Warnings []string `json:"warnings"` -} - -// populateSummary compiles the summary of verification results. -func (r *JsonVerifierReporter) populateSummary() *JsonVerifierSummary { - summary := &JsonVerifierSummary{} - summary.Total = len(r.artifacts) - - for _, artifact := range r.artifacts { - status := artifact.getStatus() - switch status { - case StatusPass: - summary.Passed++ - case StatusWarning: - summary.Warning++ - case StatusFail: - summary.Failed++ - } - } - - return summary -} - -// populateArtifacts compiles a list of artifact names included in the report, sorted alphabetically. -func (r *JsonVerifierReporter) populateArtifacts() []string { - artifacts := make([]string, 0, len(r.artifacts)) - seen := make(map[string]struct{}, len(r.artifacts)) - - for _, artifact := range r.artifacts { - if _, ok := seen[artifact.Name]; ok { - continue - } - - seen[artifact.Name] = struct{}{} - artifacts = append(artifacts, artifact.Name) - } - - sorted := slices.Clone(artifacts) - sort.Strings(sorted) - - return sorted -} - -// populateResults compiles the results for all artifacts, sorted by artifact path. -func (r *JsonVerifierReporter) populateResults() []*JsonVerifierResult { - results := make([]*JsonVerifierResult, 0, len(r.artifacts)) - - sortedArtifacts := slices.Clone(r.artifacts) - slices.SortFunc(sortedArtifacts, func(a, b *JsonVerifierState) int { - return strings.Compare(a.Path, b.Path) - }) - - for _, artifact := range sortedArtifacts { - results = append(results, artifact.FormatResult()) - } - - return results -} diff --git a/services/launcher/verifier_report_test.go b/services/launcher/verifier_report_test.go deleted file mode 100644 index 4aed9393833..00000000000 --- a/services/launcher/verifier_report_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package launcher - -import ( - "bytes" - "fmt" - "slices" - "testing" - - "www.velocidex.com/golang/velociraptor/json" -) - -// Helper function to create an AnalysisState for testing. -func state(artifact string, errors []error, warnings []string) *AnalysisState { - return &AnalysisState{ - Artifact: artifact, - Permissions: nil, - Errors: errors, - Warnings: warnings, - } -} - -func TestNewVerifierReporter(t *testing.T) { - reporter, err := NewVerifierReporter("json") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if _, ok := reporter.(*JsonVerifierReporter); !ok { - t.Fatalf("expected JsonVerifierReporter, got %T", reporter) - } - - _, err = NewVerifierReporter("unknown") - if err == nil { - t.Fatalf("expected error for unknown format, got nil") - } -} - -func TestJsonVerifierStateGetStatus(t *testing.T) { - tests := []struct { - name string - state *AnalysisState - expected resultStatus - }{ - { - name: "pass", - state: state("Artifact1", nil, nil), - expected: StatusPass, - }, - { - name: "warning", - state: state("Artifact2", nil, []string{"warning"}), - expected: StatusWarning, - }, - { - name: "fail", - state: state("Artifact3", []error{fmt.Errorf("error")}, nil), - expected: StatusFail, - }, - { - name: "fail_with_warning", - state: state("Artifact4", []error{fmt.Errorf("error")}, []string{"warning"}), - expected: StatusFail, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - jsonState := &JsonVerifierState{ - Name: tt.state.Artifact, - State: tt.state, - } - if status := jsonState.getStatus(); status != tt.expected { - t.Errorf("expected %v, got %v", tt.expected, status) - } - }) - } -} - -func TestJsonVerifierStateFormatResult(t *testing.T) { - js := &JsonVerifierState{ - Name: "Artifact1", - Path: "./Artifact1.yaml", - State: state("Artifact1", []error{fmt.Errorf("error1"), fmt.Errorf("error2")}, []string{"warning1", "warning2"}), - } - - result := js.FormatResult() - - if result.Name != "Artifact1" { - t.Errorf("expected name 'Artifact1', got '%s'", result.Name) - } - - if result.Path != "./Artifact1.yaml" { - t.Errorf("expected path './Artifact1.yaml', got '%s'", result.Path) - } - - if result.Status != string(StatusFail) { - t.Errorf("expected status 'fail', got '%s'", result.Status) - } - - if len(result.Errors) != 2 || result.Errors[0] != "error1" || result.Errors[1] != "error2" { - t.Errorf("unexpected errors: %v", result.Errors) - } - - if len(result.Warnings) != 2 || result.Warnings[0] != "warning1" || result.Warnings[1] != "warning2" { - t.Errorf("unexpected warnings: %v", result.Warnings) - } -} - -func TestJsonVerifierPopulateSummary(t *testing.T) { - report := &JsonVerifierReporter{} - report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) - report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", []error{fmt.Errorf("error")}, nil)) - report.AddArtifact("Artifact3", "./Artifact3.yaml", state("Artifact3", nil, []string{"warning"})) - report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", []error{fmt.Errorf("error")}, []string{"warning"})) - - result := report.populateSummary() - - if result.Total != 4 || result.Passed != 1 || result.Warning != 1 || result.Failed != 2 { - t.Errorf("unexpected summary: %+v", result) - } -} - -func TestJsonVerifierPopulateArtifacts(t *testing.T) { - report := &JsonVerifierReporter{} - report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", nil, nil)) - report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) - report.AddArtifact("Artifact3", "./Artifact3.yaml", state("Artifact3", nil, nil)) - - artifacts := report.populateArtifacts() - expected := []string{"Artifact1", "Artifact2", "Artifact3"} - - if !slices.Equal(artifacts, expected) { - t.Errorf("expected %v, got %v", expected, artifacts) - } -} -func TestJsonVerifierPopulateResults(t *testing.T) { - report := &JsonVerifierReporter{} - report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) - report.AddArtifact("Artifact2", "./Artifact2.yaml", state("Artifact2", nil, nil)) - - result := report.populateResults() - - if len(result) != 2 { - t.Fatalf("expected 2 results, got %d", len(result)) - } - - if result[0].Path != "./Artifact1.yaml" || result[1].Path != "./Artifact2.yaml" { - t.Errorf("results not sorted by path: %v", result) - } -} - -func TestJsonVerifierGenerate(t *testing.T) { - report := &JsonVerifierReporter{} - report.AddArtifact("Artifact1", "./Artifact1.yaml", state("Artifact1", nil, nil)) - report.SetExit(nil) - - var buf bytes.Buffer - if err := report.Generate(&buf); err != nil { - t.Fatalf("generate failed: %v", err) - } - - var result JsonVerifierFormat - if err := json.Unmarshal(buf.Bytes(), &result); err != nil { - t.Fatalf("unmarshal failed: %v", err) - } - - if result.ExitCode != 0 { - t.Errorf("expected exit code 0, got %d", result.ExitCode) - } - - if result.Summary.Passed != 1 { - t.Errorf("expected 1 passed, got %d", result.Summary.Passed) - } - - if len(result.Results) != 1 || result.Results[0].Status != "pass" { - t.Errorf("unexpected results: %v", result.Results) - } -} From e1d555708f9580b5923cab3e8d7571a087c2480a Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Mon, 5 Jan 2026 15:38:37 +1100 Subject: [PATCH 06/24] fix: increase read length to 4 MiB --- artifacts/definitions/Server/Utils/ArtifactVerifier.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml index b4b412b6eae..6b22ca6e8cd 100644 --- a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml +++ b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml @@ -40,7 +40,7 @@ sources: regex='''^name:\s*(.+)''', string=Artifact).g1 LET Files = SELECT OSPath, - read_file(filename=OSPath, length=10000) AS Data + read_file(filename=OSPath, length=4194304) AS Data FROM glob(globs=SearchGlob) LET Results <= SELECT name, From 73f9ca113d4f3387f481ae365f4cdb3f433736a9 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Tue, 6 Jan 2026 14:35:43 +1100 Subject: [PATCH 07/24] feat: initial refactor of verify command to use VQL function Refactors the `artifacts verify` command to use the `verify` VQL function which better fits Velociraptor's VQL-first design. This is only an initial implementation, it currently does not provide feature parity due to missing control over overriding built-in artifacts in the `verify` function. --- bin/verify.go | 92 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/bin/verify.go b/bin/verify.go index b424cdb4121..c29af8de5b9 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -2,16 +2,18 @@ package main import ( "fmt" - "os" + "log" + "path/filepath" + "github.com/Velocidex/ordereddict" errors "github.com/go-errors/errors" - artifacts_proto "www.velocidex.com/golang/velociraptor/artifacts/proto" - "www.velocidex.com/golang/velociraptor/constants" + "www.velocidex.com/golang/velociraptor/json" logging "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/services/launcher" "www.velocidex.com/golang/velociraptor/startup" - "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/vfilter" ) var ( @@ -46,49 +48,69 @@ func doVerify() error { return err } - logger := logging.GetLogger(config_obj, &logging.ToolComponent) - // Report all errors and keep going as much as possible. - artifacts := make(map[string]*artifacts_proto.Artifact) states := make(map[string]*launcher.AnalysisState) - repository, err := manager.GetGlobalRepository(config_obj) - if err != nil { - return err - } + logger := logging.GetLogger(config_obj, &logging.ToolComponent) - for _, artifact_path := range *verify_args { - state := launcher.NewAnalysisState(artifact_path) - states[artifact_path] = state + var artifact_paths []string - fd, err := os.Open(artifact_path) + for _, artifact_path := range *verify_args { + abs, err := filepath.Abs(artifact_path) if err != nil { - state.SetError(err) + logger.Error("verify: could not get absolute path for %v", artifact_path) continue } - data, err := utils.ReadAllWithLimit(fd, constants.MAX_MEMORY) - if err != nil { - state.SetError(err) - continue - } + artifact_paths = append(artifact_paths, abs) + } - a, err := repository.LoadYaml(string(data), services.ArtifactOptions{ - ValidateArtifact: true, - ArtifactIsBuiltIn: *verify_allow_override, - AllowOverridingAlias: true, - }) - if err != nil { - state.SetError(err) - continue - } - artifacts[artifact_path] = a + artifact_logger := &LogWriter{config_obj: sm.Config} + builder := services.ScopeBuilder{ + Config: sm.Config, + ACLManager: acl_managers.NewRoleACLManager(sm.Config, "administrator"), + Logger: log.New(artifact_logger, "", 0), + Env: ordereddict.NewDict(). + Set("Artifacts", artifact_paths), } - for artifact_path, artifact := range artifacts { - state, _ := states[artifact_path] - launcher.VerifyArtifact(ctx, config_obj, - repository, artifact, state) + query := ` + SELECT Filename, verify(artifact=Data) AS Result FROM read_file(filenames=Artifacts) + ` + + scope := manager.BuildScope(builder) + defer scope.Close() + + statements, err := vfilter.MultiParse(query) + if err != nil { + logger.Error("verify: error passing query: %v", query) + return err + } + + for _, vql := range statements { + for row := range vql.Eval(sm.Ctx, scope) { + dict := vfilter.RowToDict(ctx, scope, row) + + path, pres := dict.GetString("Filename") + if !pres { + continue + } + + result, pres := dict.Get("Result") + if !pres { + continue + } + + serialized := json.MustMarshalIndent(result) + state := &launcher.AnalysisState{} + err := json.Unmarshal(serialized, state) + if err != nil { + logger.Error("verify: could not unmarshal analysis state") + continue + } + + states[path] = state + } } var ret error From c29e55014af4bb441a357d1376027a67a961c9ae Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Tue, 6 Jan 2026 16:47:33 +1100 Subject: [PATCH 08/24] feat: add "disable_override" argument to "verify" function Adds a `disable_override` argument to the `verify` function which adds the ability control the override of built-in artifacts. This has been added to maintain backwards compability as previously built-in artifacts were overridden by default. --- docs/references/vql.yaml | 4 ++++ vql/golang/verify.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/references/vql.yaml b/docs/references/vql.yaml index 224d5053dfe..522e8eacc90 100644 --- a/docs/references/vql.yaml +++ b/docs/references/vql.yaml @@ -11588,6 +11588,10 @@ description: The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact required: true + - name: disable_override + type: bool + description: If set, we do not allow override of built-in artifacts (allowed by + default) platforms: - linux_amd64_cgo - windows_amd64_cgo diff --git a/vql/golang/verify.go b/vql/golang/verify.go index 08a299a3988..fdd06c43ac2 100644 --- a/vql/golang/verify.go +++ b/vql/golang/verify.go @@ -12,7 +12,8 @@ import ( ) type VerifyFunctionArgs struct { - Artifact string `vfilter:"required,field=artifact,doc=The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact"` + Artifact string `vfilter:"required,field=artifact,doc=The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact"` + DisableOverride bool `vfilter:"optional,field=disable_override,doc=If set, we do not allow override of built-in artifacts (allowed by default)"` } func init() { @@ -63,7 +64,7 @@ This function will verify the artifact and flag any potential errors or warnings artifact, err = local_repository.LoadYaml(arg.Artifact, services.ArtifactOptions{ ValidateArtifact: true, - ArtifactIsBuiltIn: true, + ArtifactIsBuiltIn: !arg.DisableOverride, AllowOverridingAlias: true, }) if err != nil { From 3ef69e8c29414120e615805089f51bfe8b6b12b2 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Tue, 6 Jan 2026 18:35:58 +1100 Subject: [PATCH 09/24] fix: remove local repository to fix built-in logic check Removes creation of the local artifact repository during verification to ensure that we can check for override of built-in artifacts. --- vql/golang/verify.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/vql/golang/verify.go b/vql/golang/verify.go index fdd06c43ac2..24be54e82c7 100644 --- a/vql/golang/verify.go +++ b/vql/golang/verify.go @@ -58,10 +58,7 @@ This function will verify the artifact and flag any potential errors or warnings artifact, pres := repository.Get(ctx, config_obj, arg.Artifact) if !pres { - local_repository := manager.NewRepository() - local_repository.SetParent(repository, config_obj) - - artifact, err = local_repository.LoadYaml(arg.Artifact, + artifact, err = repository.LoadYaml(arg.Artifact, services.ArtifactOptions{ ValidateArtifact: true, ArtifactIsBuiltIn: !arg.DisableOverride, @@ -71,8 +68,6 @@ This function will verify the artifact and flag any potential errors or warnings state.SetError(err) return state } - - repository = local_repository } // Verify the artifact From 2ec6a4054b9655992539c23dd91d8f542f4c96ed Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Tue, 6 Jan 2026 18:36:16 +1100 Subject: [PATCH 10/24] test: add tests for verify "disable_override" argument --- .../testdata/server/testcases/verify.in.yaml | 17 +++++++++++++ .../testdata/server/testcases/verify.out.yaml | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/artifacts/testdata/server/testcases/verify.in.yaml b/artifacts/testdata/server/testcases/verify.in.yaml index 256c42dc03b..63b264bdf65 100644 --- a/artifacts/testdata/server/testcases/verify.in.yaml +++ b/artifacts/testdata/server/testcases/verify.in.yaml @@ -18,6 +18,16 @@ Parameters: sources: - query: SELECT * FROM execve(argv=["ls"]) + Override1: | + name: Generic.Client.Info + sources: + - query: SELECT * FROM scope() + + Override2: | + name: Override2 + sources: + - query: SELECT * FROM scope() + Queries: # Basic verification of some artifacts. @@ -35,3 +45,10 @@ Queries: } AS results FROM Artifact.Server.Utils.ArtifactVerifier( SearchGlob=srcDir+"/artifacts/testdata/files/artifacts/*") + + # Check the "disable_override" argument produces an error when overriding a built-in, + # but not when creating a new artifact when True is specified. + - SELECT + verify(artifact=Override1, disable_override=True) AS Override1, + verify(artifact=Override2, disable_override=True) AS Override2 + FROM scope() \ No newline at end of file diff --git a/artifacts/testdata/server/testcases/verify.out.yaml b/artifacts/testdata/server/testcases/verify.out.yaml index 4e5e8dae204..87636005594 100644 --- a/artifacts/testdata/server/testcases/verify.out.yaml +++ b/artifacts/testdata/server/testcases/verify.out.yaml @@ -99,3 +99,27 @@ Output: [ } ] +# Check the "disable_override" argument produces an error when overriding a built-in, +# but not when creating a new artifact when True is specified. +Query: SELECT verify(artifact=Override1, disable_override=True) AS Override1, verify(artifact=Override2, disable_override=True) AS Override2 FROM scope() +Output: [ + { + "Override1": { + "Artifact": "name: Generic.Client.Info\nsources:\n- query: SELECT * FROM scope()\n", + "Permissions": null, + "Errors": [ + "Unable to override built in artifact Generic.Client.Info" + ], + "Warnings": null, + "Definitions": {} + }, + "Override2": { + "Artifact": "name: Override2\nsources:\n- query: SELECT * FROM scope()\n", + "Permissions": null, + "Errors": null, + "Warnings": null, + "Definitions": {} + } + } +] + From 892e130e5ce04fe88c193274badba00134280d28 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Tue, 6 Jan 2026 18:49:18 +1100 Subject: [PATCH 11/24] feat: re-implement "builtin" flag functionality for verify command Re-implements the `--builtin` flag functionality for the `verify` command for feature parity and backwards compatibility. --- bin/verify.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/verify.go b/bin/verify.go index c29af8de5b9..bcd6e852c88 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -71,11 +71,12 @@ func doVerify() error { ACLManager: acl_managers.NewRoleACLManager(sm.Config, "administrator"), Logger: log.New(artifact_logger, "", 0), Env: ordereddict.NewDict(). - Set("Artifacts", artifact_paths), + Set("Artifacts", artifact_paths). + Set("DisableOverride", !*verify_allow_override), } query := ` - SELECT Filename, verify(artifact=Data) AS Result FROM read_file(filenames=Artifacts) + SELECT Filename, verify(artifact=Data, disable_override=DisableOverride) AS Result FROM read_file(filenames=Artifacts) ` scope := manager.BuildScope(builder) From 83781672c7be9ab68ad5dd691396f376a6fa6cd5 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 10:26:09 +1100 Subject: [PATCH 12/24] feat: initial implementation of "repository" argument in artifact_set function --- vql/server/repository.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/vql/server/repository.go b/vql/server/repository.go index 6e38ced4a38..17e182520da 100644 --- a/vql/server/repository.go +++ b/vql/server/repository.go @@ -21,6 +21,7 @@ type ArtifactSetFunctionArgs struct { Definition string `vfilter:"optional,field=definition,doc=Artifact definition in YAML"` Prefix string `vfilter:"optional,field=prefix,doc=Optional name prefix (deprecated ignored)"` Tags []string `vfilter:"optional,field=tags,doc=Optional tags to attach to the artifact."` + Repository string `vfilter:"optional,field=repository,doc=Add the artifact to this repository, if not set, we add the artifact to the global repository."` } type ArtifactSetFunction struct{} @@ -87,6 +88,40 @@ func (self *ArtifactSetFunction) Call(ctx context.Context, principal := vql_subsystem.GetPrincipal(scope) + global_repository, err := manager.GetGlobalRepository(config_obj) + if err != nil { + scope.Log("artifact_set: %s", err) + return vfilter.Null{} + } + + if arg.Repository != "" { + var local_repository services.Repository + cached_any := vql_subsystem.CacheGet(scope, arg.Repository) + + if cached_repository, ok := cached_any.(services.Repository); ok { + local_repository = cached_repository + } else { + scope.Log("artifact_set: creating new repository '%s'", arg.Repository) + local_repository = manager.NewRepository() + local_repository.SetParent(global_repository, config_obj) + } + + definition, err := local_repository.LoadYaml(arg.Definition, + services.ArtifactOptions{ + ValidateArtifact: true, + ArtifactIsBuiltIn: true, + }) + if err != nil { + scope.Log("artifact_set: %s", err) + return vfilter.Null{} + } + + scope.Log("artifact_set: added %s to repository '%s'", definition.Name, arg.Repository) + vql_subsystem.CacheSet(scope, arg.Repository, local_repository) + + return json.ConvertProtoToOrderedDict(definition) + } + definition, err = manager.SetArtifactFile(ctx, config_obj, principal, arg.Definition, arg.Prefix) if err != nil { From 18412a389b5ad72e0f657d5b8e222f6f73e03fbf Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 11:50:21 +1100 Subject: [PATCH 13/24] feat: initial implementation of "repository" argument in verify function --- vql/golang/verify.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vql/golang/verify.go b/vql/golang/verify.go index 24be54e82c7..c36b26b4e4f 100644 --- a/vql/golang/verify.go +++ b/vql/golang/verify.go @@ -13,6 +13,7 @@ import ( type VerifyFunctionArgs struct { Artifact string `vfilter:"required,field=artifact,doc=The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact"` + Repository string `vfilter:"optional,field=repository,doc=The repository to use for verification, if not set, we default to the global repository."` DisableOverride bool `vfilter:"optional,field=disable_override,doc=If set, we do not allow override of built-in artifacts (allowed by default)"` } @@ -56,6 +57,14 @@ This function will verify the artifact and flag any potential errors or warnings state := launcher.NewAnalysisState(arg.Artifact) + if arg.Repository != "" { + cached_any := vql_subsystem.CacheGet(scope, arg.Repository) + + if cached_repository, ok := cached_any.(services.Repository); ok { + repository = cached_repository + } + } + artifact, pres := repository.Get(ctx, config_obj, arg.Artifact) if !pres { artifact, err = repository.LoadYaml(arg.Artifact, From 83d96e12219ce1c910ee8c47c0c2aacacec66e69 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 11:50:57 +1100 Subject: [PATCH 14/24] feat: initial implementation of local repository in verify command --- bin/verify.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/verify.go b/bin/verify.go index bcd6e852c88..b4ad59f6773 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -76,7 +76,14 @@ func doVerify() error { } query := ` - SELECT Filename, verify(artifact=Data, disable_override=DisableOverride) AS Result FROM read_file(filenames=Artifacts) + -- Load artifacts into local repository + LET Definitions = SELECT Filename, artifact_set(definition=Data, repository="local") AS Definition FROM read_file(filenames=Artifacts) + + -- Verify artifacts from local repository + SELECT Filename, Result FROM foreach( + row=Definitions, + query={ SELECT Filename, verify(artifact=Definition.name, repository="local") AS Result FROM scope() } + ) ` scope := manager.BuildScope(builder) From 50b3346ed3c1b25f4f553083d1a69eff1c310f75 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 11:55:36 +1100 Subject: [PATCH 15/24] docs: update vql reference docs --- docs/references/vql.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/references/vql.yaml b/docs/references/vql.yaml index 522e8eacc90..b30b9b4870c 100644 --- a/docs/references/vql.yaml +++ b/docs/references/vql.yaml @@ -264,6 +264,10 @@ type: string description: Optional tags to attach to the artifact. repeated: true + - name: repository + type: string + description: Add the artifact to this repository, if not set, we add the artifact + to the global repository. category: server metadata: permissions: ARTIFACT_WRITER,SERVER_ARTIFACT_WRITER @@ -11588,6 +11592,10 @@ description: The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact required: true + - name: repository + type: string + description: The repository to use for verification, if not set, we default to + the global repository. - name: disable_override type: bool description: If set, we do not allow override of built-in artifacts (allowed by From 9a2ff298c6ec6f05303ee4bf9ad6bb832ff87e32 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 12:06:07 +1100 Subject: [PATCH 16/24] fix: materialize definitions and add filter --- bin/verify.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/verify.go b/bin/verify.go index b4ad59f6773..0f44896f5ee 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -77,12 +77,12 @@ func doVerify() error { query := ` -- Load artifacts into local repository - LET Definitions = SELECT Filename, artifact_set(definition=Data, repository="local") AS Definition FROM read_file(filenames=Artifacts) + LET Definitions <= SELECT Filename, artifact_set(definition=Data, repository="local") AS Definition FROM read_file(filenames=Artifacts) -- Verify artifacts from local repository SELECT Filename, Result FROM foreach( row=Definitions, - query={ SELECT Filename, verify(artifact=Definition.name, repository="local") AS Result FROM scope() } + query={ SELECT Filename, verify(artifact=Definition.name, repository="local") AS Result FROM scope() WHERE Definition } ) ` From 07b872659417c852323e68b4ada813e31ce1f8c2 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 12:13:56 +1100 Subject: [PATCH 17/24] fix: add tag to repository cache key --- vql/golang/verify.go | 6 +++++- vql/server/repository.go | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/vql/golang/verify.go b/vql/golang/verify.go index c36b26b4e4f..f373f0bc8fe 100644 --- a/vql/golang/verify.go +++ b/vql/golang/verify.go @@ -11,6 +11,10 @@ import ( "www.velocidex.com/golang/vfilter/arg_parser" ) +const ( + REPOSITORY_CACHE_TAG = "__REPOSITORY_" +) + type VerifyFunctionArgs struct { Artifact string `vfilter:"required,field=artifact,doc=The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact"` Repository string `vfilter:"optional,field=repository,doc=The repository to use for verification, if not set, we default to the global repository."` @@ -58,7 +62,7 @@ This function will verify the artifact and flag any potential errors or warnings state := launcher.NewAnalysisState(arg.Artifact) if arg.Repository != "" { - cached_any := vql_subsystem.CacheGet(scope, arg.Repository) + cached_any := vql_subsystem.CacheGet(scope, REPOSITORY_CACHE_TAG+arg.Repository) if cached_repository, ok := cached_any.(services.Repository); ok { repository = cached_repository diff --git a/vql/server/repository.go b/vql/server/repository.go index 17e182520da..0eac51cf5dd 100644 --- a/vql/server/repository.go +++ b/vql/server/repository.go @@ -17,6 +17,10 @@ import ( "www.velocidex.com/golang/vfilter/arg_parser" ) +const ( + REPOSITORY_CACHE_TAG = "__REPOSITORY_" +) + type ArtifactSetFunctionArgs struct { Definition string `vfilter:"optional,field=definition,doc=Artifact definition in YAML"` Prefix string `vfilter:"optional,field=prefix,doc=Optional name prefix (deprecated ignored)"` @@ -96,7 +100,7 @@ func (self *ArtifactSetFunction) Call(ctx context.Context, if arg.Repository != "" { var local_repository services.Repository - cached_any := vql_subsystem.CacheGet(scope, arg.Repository) + cached_any := vql_subsystem.CacheGet(scope, REPOSITORY_CACHE_TAG+arg.Repository) if cached_repository, ok := cached_any.(services.Repository); ok { local_repository = cached_repository @@ -117,7 +121,7 @@ func (self *ArtifactSetFunction) Call(ctx context.Context, } scope.Log("artifact_set: added %s to repository '%s'", definition.Name, arg.Repository) - vql_subsystem.CacheSet(scope, arg.Repository, local_repository) + vql_subsystem.CacheSet(scope, REPOSITORY_CACHE_TAG+arg.Repository, local_repository) return json.ConvertProtoToOrderedDict(definition) } From 7440fe8400a89e1e8b34d52f7a67ae1d5f2abb98 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 12:49:13 +1100 Subject: [PATCH 18/24] fix: import repository cache key constant --- vql/golang/verify.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vql/golang/verify.go b/vql/golang/verify.go index f373f0bc8fe..f72bee1c06b 100644 --- a/vql/golang/verify.go +++ b/vql/golang/verify.go @@ -7,14 +7,11 @@ import ( "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/services/launcher" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + vql_server "www.velocidex.com/golang/velociraptor/vql/server" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) -const ( - REPOSITORY_CACHE_TAG = "__REPOSITORY_" -) - type VerifyFunctionArgs struct { Artifact string `vfilter:"required,field=artifact,doc=The artifact to verify. This can be an artifact source in yaml or json or the name of an artifact"` Repository string `vfilter:"optional,field=repository,doc=The repository to use for verification, if not set, we default to the global repository."` @@ -62,7 +59,7 @@ This function will verify the artifact and flag any potential errors or warnings state := launcher.NewAnalysisState(arg.Artifact) if arg.Repository != "" { - cached_any := vql_subsystem.CacheGet(scope, REPOSITORY_CACHE_TAG+arg.Repository) + cached_any := vql_subsystem.CacheGet(scope, vql_server.REPOSITORY_CACHE_TAG+arg.Repository) if cached_repository, ok := cached_any.(services.Repository); ok { repository = cached_repository From 0d6a5d50186724208f034bd947025afb10baa843 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 7 Jan 2026 12:46:10 +1000 Subject: [PATCH 19/24] Some bugfixes 1. Order of artifact evaluation was non deterministic due to use of map 2. If an artifact was not able to load it would not produce any error. 3. The Server.Utils.ArtifactVerifier did not account for dependencies of other custom artifacts. --- .../Server/Utils/ArtifactVerifier.yaml | 8 ++- .../testdata/files/artifacts/caller.yaml | 3 ++ artifacts/testdata/files/artifacts/info.yaml | 3 ++ .../testdata/server/testcases/verify.out.yaml | 20 +++++++- bin/verify.go | 50 ++++++++----------- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 artifacts/testdata/files/artifacts/caller.yaml create mode 100644 artifacts/testdata/files/artifacts/info.yaml diff --git a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml index 6b22ca6e8cd..170e75ed2db 100644 --- a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml +++ b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml @@ -43,16 +43,20 @@ sources: read_file(filename=OSPath, length=4194304) AS Data FROM glob(globs=SearchGlob) + LET Artifacts <= SELECT *, + artifact_set(definition=Data, repository="local") AS Definition + FROM Files + LET Results <= SELECT name, path, PassLogError(Verify=Verify, Path=path) AS passed, Stringify(X=Verify.Errors) AS errors, Stringify(X=Verify.Warnings) AS warnings - FROM foreach(row=Files, + FROM foreach(row=Artifacts, query={ SELECT OSPath AS path, GetName(Artifact=Data) AS name, - verify(artifact=Data) AS Verify + verify(artifact=Data, repository="local") AS Verify FROM scope() }) diff --git a/artifacts/testdata/files/artifacts/caller.yaml b/artifacts/testdata/files/artifacts/caller.yaml new file mode 100644 index 00000000000..ac560106c0d --- /dev/null +++ b/artifacts/testdata/files/artifacts/caller.yaml @@ -0,0 +1,3 @@ +name: Caller +sources: +- query: SELECT * FROM Artifact.Good() diff --git a/artifacts/testdata/files/artifacts/info.yaml b/artifacts/testdata/files/artifacts/info.yaml new file mode 100644 index 00000000000..36a919f4809 --- /dev/null +++ b/artifacts/testdata/files/artifacts/info.yaml @@ -0,0 +1,3 @@ +name: Generic.Client.Info +sources: +- query: SELECT * FROM info() diff --git a/artifacts/testdata/server/testcases/verify.out.yaml b/artifacts/testdata/server/testcases/verify.out.yaml index 87636005594..bff4a77f5a9 100644 --- a/artifacts/testdata/server/testcases/verify.out.yaml +++ b/artifacts/testdata/server/testcases/verify.out.yaml @@ -49,18 +49,27 @@ Query: SELECT summary, artifacts, { SELECT *, basename(path=path) as path FROM r Output: [ { "summary": { - "total": 4, - "passed": 2, + "total": 6, + "passed": 4, "failed": 2, "warnings": 1 }, "artifacts": [ + "Caller", "Good", + "Generic.Client.Info", "BrokenYaml", "BrokenQuery", "WarnningExecve" ], "results": [ + { + "name": "Caller", + "passed": true, + "errors": [], + "warnings": [], + "path": "caller.yaml" + }, { "name": "Good", "passed": true, @@ -68,6 +77,13 @@ Output: [ "warnings": [], "path": "good.yaml" }, + { + "name": "Generic.Client.Info", + "passed": true, + "errors": [], + "warnings": [], + "path": "info.yaml" + }, { "name": "BrokenYaml", "passed": false, diff --git a/bin/verify.go b/bin/verify.go index 0f44896f5ee..a99a271fc44 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -7,7 +7,6 @@ import ( "github.com/Velocidex/ordereddict" errors "github.com/go-errors/errors" - "www.velocidex.com/golang/velociraptor/json" logging "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/services/launcher" @@ -48,9 +47,6 @@ func doVerify() error { return err } - // Report all errors and keep going as much as possible. - states := make(map[string]*launcher.AnalysisState) - logger := logging.GetLogger(config_obj, &logging.ToolComponent) var artifact_paths []string @@ -77,13 +73,17 @@ func doVerify() error { query := ` -- Load artifacts into local repository - LET Definitions <= SELECT Filename, artifact_set(definition=Data, repository="local") AS Definition FROM read_file(filenames=Artifacts) + LET Definitions <= SELECT Filename, Data, + artifact_set(definition=Data, repository="local") AS Definition + FROM read_file(filenames=Artifacts) -- Verify artifacts from local repository SELECT Filename, Result FROM foreach( row=Definitions, - query={ SELECT Filename, verify(artifact=Definition.name, repository="local") AS Result FROM scope() WHERE Definition } - ) + query={ + SELECT Filename, verify(artifact=Data, repository="local") AS Result + FROM scope() + }) ` scope := manager.BuildScope(builder) @@ -95,11 +95,12 @@ func doVerify() error { return err } + var ret error for _, vql := range statements { for row := range vql.Eval(sm.Ctx, scope) { dict := vfilter.RowToDict(ctx, scope, row) - path, pres := dict.GetString("Filename") + artifact_path, pres := dict.GetString("Filename") if !pres { continue } @@ -109,29 +110,20 @@ func doVerify() error { continue } - serialized := json.MustMarshalIndent(result) - state := &launcher.AnalysisState{} - err := json.Unmarshal(serialized, state) - if err != nil { - logger.Error("verify: could not unmarshal analysis state") + state, ok := result.(*launcher.AnalysisState) + if !ok { continue } - - states[path] = state - } - } - - var ret error - for artifact_path, state := range states { - if len(state.Errors) == 0 { - logger.Info("Verified %v: OK", artifact_path) - } - for _, err := range state.Errors { - logger.Error("%v: %v", artifact_path, err) - ret = errors.New(err) - } - for _, msg := range state.Warnings { - logger.Info("%v: %v", artifact_path, msg) + if len(state.Errors) == 0 { + logger.Info("Verified %v: OK", artifact_path) + } + for _, err := range state.Errors { + logger.Error("%v: %v", artifact_path, err) + ret = errors.New(err) + } + for _, msg := range state.Warnings { + logger.Info("%v: %v", artifact_path, msg) + } } } From a9ad8af2b3b6de36ce61b648b997fc7b34deb2f2 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 14:47:07 +1100 Subject: [PATCH 20/24] fix: add missing "disable_override" argument from query --- bin/verify.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/verify.go b/bin/verify.go index a99a271fc44..b6754a52f35 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -81,7 +81,8 @@ func doVerify() error { SELECT Filename, Result FROM foreach( row=Definitions, query={ - SELECT Filename, verify(artifact=Data, repository="local") AS Result + SELECT Filename, + verify(artifact=Data, repository="local", disable_override=DisableOverride) AS Result FROM scope() }) ` From 0ab2d188a66fd578d55e8526cbfd9dbf84f988b0 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 14:48:01 +1100 Subject: [PATCH 21/24] fix: add check for built-in artifacts in set_artifact --- vql/server/repository.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/vql/server/repository.go b/vql/server/repository.go index 0eac51cf5dd..017619f649c 100644 --- a/vql/server/repository.go +++ b/vql/server/repository.go @@ -110,11 +110,26 @@ func (self *ArtifactSetFunction) Call(ctx context.Context, local_repository.SetParent(global_repository, config_obj) } - definition, err := local_repository.LoadYaml(arg.Definition, + // Determine if this is a built-in artifact + tmp_repository := local_repository.Copy() + built_in := false + + artifact, err := tmp_repository.LoadYaml(arg.Definition, services.ArtifactOptions{ ValidateArtifact: true, ArtifactIsBuiltIn: true, }) + if err == nil { + if global_artifact, pres := global_repository.Get(ctx, config_obj, artifact.Name); pres { + built_in = global_artifact.BuiltIn + } + } + + definition, err := local_repository.LoadYaml(arg.Definition, + services.ArtifactOptions{ + ValidateArtifact: true, + ArtifactIsBuiltIn: built_in, + }) if err != nil { scope.Log("artifact_set: %s", err) return vfilter.Null{} From 17aa307b4303488f736fd11bd1d491b8e5495a30 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 14:55:11 +1100 Subject: [PATCH 22/24] fix: format query VQL --- bin/verify.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/bin/verify.go b/bin/verify.go index b6754a52f35..0cff08d3669 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -73,18 +73,23 @@ func doVerify() error { query := ` -- Load artifacts into local repository - LET Definitions <= SELECT Filename, Data, - artifact_set(definition=Data, repository="local") AS Definition - FROM read_file(filenames=Artifacts) + LET Definitions <= SELECT + Filename, + Data, + artifact_set(definition=Data, repository="local") AS Definition + FROM read_file(filenames=Artifacts) -- Verify artifacts from local repository - SELECT Filename, Result FROM foreach( - row=Definitions, - query={ - SELECT Filename, - verify(artifact=Data, repository="local", disable_override=DisableOverride) AS Result - FROM scope() - }) + SELECT Filename, + Result + FROM foreach(row=Definitions, + query={ + SELECT Filename, + verify(artifact=Data, + repository="local", + disable_override=DisableOverride) AS Result + FROM scope() + }) ` scope := manager.BuildScope(builder) From b5b15d8d5d20be28b7aa7a8ce825c161754fdb1a Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 15:06:39 +1100 Subject: [PATCH 23/24] fix: remove length from read_file Removed `length` argument from call to `read_file` as it's implicitly set to 4 MiB. This should be enough to cover most artifacts. --- artifacts/definitions/Server/Utils/ArtifactVerifier.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml index 170e75ed2db..4f4ed6deb2a 100644 --- a/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml +++ b/artifacts/definitions/Server/Utils/ArtifactVerifier.yaml @@ -40,7 +40,7 @@ sources: regex='''^name:\s*(.+)''', string=Artifact).g1 LET Files = SELECT OSPath, - read_file(filename=OSPath, length=4194304) AS Data + read_file(filename=OSPath) AS Data FROM glob(globs=SearchGlob) LET Artifacts <= SELECT *, From 5a7021d00babb42f96f4de2fd35b6780a1d9a7d7 Mon Sep 17 00:00:00 2001 From: nullifysecurity Date: Wed, 7 Jan 2026 15:12:32 +1100 Subject: [PATCH 24/24] feat: add "issues_only" flag to "verify" command Adds a `issues_only` flag to the `verify` command that suppresses output of successful artifact verifications. This can be used to remove noise in the command output. --- bin/verify.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/verify.go b/bin/verify.go index 0cff08d3669..627fa8dd16f 100644 --- a/bin/verify.go +++ b/bin/verify.go @@ -19,6 +19,7 @@ var ( verify = artifact_command.Command("verify", "Verify a set of artifacts") verify_args = verify.Arg("paths", "Paths to artifact yaml files").Required().Strings() verify_allow_override = verify.Flag("builtin", "Allow overriding of built in artifacts").Bool() + verify_issues_only = verify.Flag("issues_only", "If set, we only emit warning and error messages").Bool() ) func doVerify() error { @@ -121,7 +122,9 @@ func doVerify() error { continue } if len(state.Errors) == 0 { - logger.Info("Verified %v: OK", artifact_path) + if !*verify_issues_only { + logger.Info("Verified %v: OK", artifact_path) + } } for _, err := range state.Errors { logger.Error("%v: %v", artifact_path, err)