Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion thorlog/common/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type LogEventMetadata struct {
Mod string `json:"module" textlog:"module"`
// The ID of the scan where this event was created.
ScanID string `json:"scan_id" textlog:"scanid,omitempty"`
// A unique ID for this finding.
// A unique ID for this event.
// The ID is transient and the same element may have different IDs across multiple scans.
GenID string `json:"event_id,omitempty" textlog:"uid,omitempty"`
// The hostname of the machine where this event was generated.
Expand Down
2 changes: 1 addition & 1 deletion thorlog/jsonschema/generateschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func main() {
Title: "ThorEvent",
OneOf: []*jsonschema.Schema{
{
Ref: "#/$defs/Finding",
Ref: "#/$defs/Assessment",
},
{
Ref: "#/$defs/Message",
Expand Down
10 changes: 5 additions & 5 deletions thorlog/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ func TestParseEvent(t *testing.T) {
},
},
{
"JsonV3Finding",
`{"type":"THOR finding","meta":{"time":"2024-09-24T14:18:46.190394329+02:00","level":"Alert","module":"Test","scan_id":"abdc","event_id":"abdas","hostname":"aserarsd"},"message":"This is a test finding","subject":{"type":"file","path":"path/to/file"},"score":70,"reasons":[{"type":"reason","summary":"Reason 1","signature":{"score":70,"ref":null,"origin":"internal","kind":""},"matched":null}],"reason_count":0,"context":[{"object":{"type":"at job"},"relation":"","unique":false}],"log_version":"v3"}`,
&thorlog.Finding{
"JsonV3Assessment",
`{"type":"THOR assessment","meta":{"time":"2024-09-24T14:18:46.190394329+02:00","level":"Alert","module":"Test","scan_id":"abdc","event_id":"abdas","hostname":"aserarsd"},"message":"This is a test assessment","subject":{"type":"file","path":"path/to/file"},"score":70,"reasons":[{"type":"reason","summary":"Reason 1","signature":{"score":70,"ref":null,"origin":"internal","kind":""},"matched":null}],"reason_count":0,"context":[{"object":{"type":"at job"},"relation":"","unique":false}],"log_version":"v3"}`,
&thorlog.Assessment{
ObjectHeader: jsonlog.ObjectHeader{
Type: "THOR finding",
Type: "THOR assessment",
},
Meta: thorlog.LogEventMetadata{
Time: mustTime("2024-09-24T14:18:46.190394329+02:00"),
Expand All @@ -143,7 +143,7 @@ func TestParseEvent(t *testing.T) {
GenID: "abdas",
Source: "aserarsd",
},
Text: "This is a test finding",
Text: "This is a test assessment",
Subject: &thorlog.File{
ObjectHeader: jsonlog.ObjectHeader{
Type: "file",
Expand Down
66 changes: 33 additions & 33 deletions thorlog/v3/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import (
"golang.org/x/exp/slices"
)

// Finding is a summary of a Subject's analysis by THOR.
// This object is usually, but not necessarily suspicious; the
// Assessment is a summary of a Subject's analysis by THOR.
// The assessed object is not necessarily suspicious; the
// severity can be seen in the Score, and beyond that the
// Reasons contain further information on why this Subject is
// Reasons contain further information if this Subject is
// considered suspicious.
type Finding struct {
type Assessment struct {
jsonlog.ObjectHeader
Meta LogEventMetadata `json:"meta" textlog:",expand"`
// Text is the message THOR printed for this finding.
// This is usually a summary based on this finding's subject and level.
// Text is the message THOR printed for this assessment.
// This is usually a summary based on this assessment's subject and level.
Text string `json:"message" textlog:"message"`
// Subject is the object analysed by THOR.
Subject ReportableObject `json:"subject" textlog:",expand"`
// Subject is the object assessed by THOR.
Subject AssessableObject `json:"subject" textlog:",expand"`
// Score is a metric that combines severity and certainty. The score is always in a range of 0 to 100;
// 0 indicates that the analysis found no suspicious indicators, whereas 100 indicates very high
// 0 indicates that the assessment found no suspicious indicators, whereas 100 indicates very high
// severity and certainty.
Score int64 `json:"score" textlog:"score"`
// Reasons describes the indicators that contributed to the score.
Expand All @@ -45,46 +45,46 @@ type Finding struct {
// and a relation name of "parent", indicating that the Subject derives from this object,
// which is its parent.
EventContext Context `json:"context" textlog:",expand" jsonschema:"nullable"`
// Issues lists any problems that THOR encountered when trying to create a Finding for this analysis.
// Issues lists any problems that THOR encountered when trying to create a JSON struct for this assessment.
// This may include e.g. overly long fields that were truncated, fields that could not be rendered to JSON,
// or similar problems.
Issues []Issue `json:"issues,omitempty" textlog:"-"`
// LogVersion describes the jsonlog version that this event was created with.
LogVersion common.Version `json:"log_version"`
}

// ReportableObject can be any object type that THOR analyses, e.g. File or Process.
type ReportableObject interface {
// AssessableObject can be any object type that THOR assesses, e.g. File or Process.
type AssessableObject interface {
reportable()
jsonlog.Object
}

func (f *Finding) Message() string {
func (f *Assessment) Message() string {
return f.Text
}

func (f *Finding) Version() common.Version {
func (f *Assessment) Version() common.Version {
return f.LogVersion
}

func (f *Finding) Metadata() *LogEventMetadata {
func (f *Assessment) Metadata() *LogEventMetadata {
return &f.Meta
}

func (f *Finding) UnmarshalJSON(data []byte) error {
type plainFinding Finding
var rawFinding struct {
plainFinding // Embed without unmarshal method to avoid infinite recursion
Subject EmbeddedObject `json:"subject"` // EmbeddedObject is used to allow unmarshalling of the subject as a ReportableObject
func (f *Assessment) UnmarshalJSON(data []byte) error {
type plainAssessment Assessment
var rawAssessment struct {
plainAssessment // Embed without unmarshal method to avoid infinite recursion
Subject EmbeddedObject `json:"subject"` // EmbeddedObject is used to allow unmarshalling of the subject as a AssessableObject
}
if err := json.Unmarshal(data, &rawFinding); err != nil {
if err := json.Unmarshal(data, &rawAssessment); err != nil {
return err
}
subject, ok := rawFinding.Subject.Object.(ReportableObject)
subject, ok := rawAssessment.Subject.Object.(AssessableObject)
if !ok {
return fmt.Errorf("subject must implement the reportable interface")
}
*f = Finding(rawFinding.plainFinding) // Copy the fields from rawFinding to f
*f = Assessment(rawAssessment.plainAssessment) // Copy the fields from rawAssessment to f
f.Subject = subject

// Resolve all references
Expand Down Expand Up @@ -115,14 +115,14 @@ func (f *Finding) UnmarshalJSON(data []byte) error {
return nil
}

var _ common.Event = (*Finding)(nil)
var _ common.Event = (*Assessment)(nil)

type Context []ContextObject

// ContextObject describes a relation of an object to another.
type ContextObject struct {
Object ReportableObject `json:"object" textlog:",expand"`
// Relations describes how the object relates to the main subject of the finding.
Object AssessableObject `json:"object" textlog:",expand"`
// Relations describes how the object relates to the assessed subject.
// There may be multiple relations, e.g. if the object is both the parent and the topmost ancestor of the subject.
//
// Relations should be ordered by relevance, i.e. the most important relation should be first.
Expand All @@ -145,7 +145,7 @@ func (c *ContextObject) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &rawContextObject); err != nil {
return err
}
reportableObject, isReportable := rawContextObject.Object.Object.(ReportableObject)
reportableObject, isReportable := rawContextObject.Object.Object.(AssessableObject)
if !isReportable {
return fmt.Errorf("object of type %q must implement the reportable interface", rawContextObject.Object.Object.EmbeddedHeader().Type)
}
Expand Down Expand Up @@ -207,14 +207,14 @@ func (c Context) MarshalTextLog(t jsonlog.TextlogFormatter) jsonlog.TextlogEntry
return result
}

const typeFinding = "THOR finding"
const typeAssessment = "THOR assessment"

func init() { AddLogObjectType(typeFinding, &Finding{}) }
func init() { AddLogObjectType(typeAssessment, &Assessment{}) }

func NewFinding(subject ReportableObject, message string) *Finding {
return &Finding{
func NewAssessment(subject AssessableObject, message string) *Assessment {
return &Assessment{
ObjectHeader: LogObjectHeader{
Type: typeFinding,
Type: typeAssessment,
},
Text: message,
Subject: subject,
Expand All @@ -223,7 +223,7 @@ func NewFinding(subject ReportableObject, message string) *Finding {
}

// Message describes a THOR message printed during the scan.
// Unlike Finding, this does not describe an analysis' result,
// Unlike Assessment, this does not describe an analysis' result,
// but rather something about the scan itself (e.g. how many IOCs were loaded).
type Message struct {
jsonlog.ObjectHeader
Expand Down
30 changes: 15 additions & 15 deletions thorlog/v3/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,18 @@ func concatEntry(entry jsonlog.TextlogEntry) string {
return builder.String()
}

func TestFinding_UnmarshalJSON(t *testing.T) {
for i, finding := range []*Finding{
func TestAssessment_UnmarshalJSON(t *testing.T) {
for i, assessment := range []*Assessment{
{
ObjectHeader: LogObjectHeader{Type: typeFinding},
ObjectHeader: LogObjectHeader{Type: typeAssessment},
Meta: LogEventMetadata{
Lvl: common.Alert,
Mod: "Test",
ScanID: "abdc",
GenID: "abdas",
Source: "aserarsd",
},
Text: "This is a test finding",
Text: "This is a test assessment",
Subject: NewFile("path/to/file"),
EventContext: Context{
{
Expand All @@ -146,27 +146,27 @@ func TestFinding_UnmarshalJSON(t *testing.T) {
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
jsonform, err := json.Marshal(finding)
jsonform, err := json.Marshal(assessment)
if err != nil {
t.Fatal(err)
}
t.Log(string(jsonform))
var newFinding Finding
if err := json.Unmarshal(jsonform, &newFinding); err != nil {
var newAssessment Assessment
if err := json.Unmarshal(jsonform, &newAssessment); err != nil {
t.Fatal(err)
}
newFinding.LogVersion = common.Version(newFinding.LogVersion.Major())
if !reflect.DeepEqual(finding, &newFinding) {
t.Errorf("UnmarshalJSON() = %+v, want %+v", newFinding, *finding)
newAssessment.LogVersion = common.Version(newAssessment.LogVersion.Major())
if !reflect.DeepEqual(assessment, &newAssessment) {
t.Errorf("UnmarshalJSON() = %+v, want %+v", newAssessment, *assessment)
}
})
}
}

func TestFinding_UnmarshalIssue(t *testing.T) {
finding := `{"type":"THOR finding","meta":{"time":"2025-07-01T12:05:12.993789131+02:00","level":"Info","module":"ProcessCheck","scan_id":"S-pSxgCmyvvfs","event_id":"","hostname":"dummy"},"message":"process found","subject":{"type":"process","pid":502168,"name":"chromium","command":"/usr/lib/chromium/chromium","owner":"owner","image":{"type":"file","path":"/usr/lib/chromium/chromium","exists":"yes","extension":"","magic_header":"ELF","hashes":{"md5":"fc04ee20f064adc18e370c22512e268e","sha1":"2c8b7d05d25e04db9c169ce85e8e8f84321ef0c8","sha256":"0cf1727aa8dc3995d5aa103001f656b8ee8a1b3ffbc6d8664c5ad95cf225771f"},"first_bytes":{"hex":"7f454c4602010100000000000000000003003e00","ascii":"ELF\u003e"},"file_times":{"modified":"2025-06-25T19:45:43+02:00","accessed":"2025-07-01T08:46:56.750309598+02:00","changed":"2025-06-26T08:39:59.980605063+02:00"},"size":252546120,"permissions":{"type":"Unix permissions","owner":"root","group":"root","mask":{"user":{"readable":true,"writable":true,"executable":true},"group":{"readable":true,"writable":false,"executable":true},"world":{"readable":true,"writable":false,"executable":true}}}},"parent_info":{"pid":9011,"exe":"/usr/lib/chromium/chromium","command":"/usr/lib/chromium/chromium"},"tree":["/usr/lib/chromium/chromium","/usr/lib/chromium/chromium"],"created":"2025-07-01T12:00:05+02:00","session":"","listen_ports":null,"connections":[]},"score":0,"reasons":null,"reason_count":0,"context":null,"issues":[{"affected":"/subject/sections","category":"truncated","description":"Removed some sections from process memory (originally 638)"}],"log_version":"v3.0.0"}`
var findingObj Finding
if err := json.Unmarshal([]byte(finding), &findingObj); err != nil {
t.Fatalf("Failed to unmarshal finding: %v", err)
func TestAssessment_UnmarshalIssue(t *testing.T) {
assessment := `{"type":"THOR assessment","meta":{"time":"2025-07-01T12:05:12.993789131+02:00","level":"Info","module":"ProcessCheck","scan_id":"S-pSxgCmyvvfs","event_id":"","hostname":"dummy"},"message":"process found","subject":{"type":"process","pid":502168,"name":"chromium","command":"/usr/lib/chromium/chromium","owner":"owner","image":{"type":"file","path":"/usr/lib/chromium/chromium","exists":"yes","extension":"","magic_header":"ELF","hashes":{"md5":"fc04ee20f064adc18e370c22512e268e","sha1":"2c8b7d05d25e04db9c169ce85e8e8f84321ef0c8","sha256":"0cf1727aa8dc3995d5aa103001f656b8ee8a1b3ffbc6d8664c5ad95cf225771f"},"first_bytes":{"hex":"7f454c4602010100000000000000000003003e00","ascii":"ELF\u003e"},"file_times":{"modified":"2025-06-25T19:45:43+02:00","accessed":"2025-07-01T08:46:56.750309598+02:00","changed":"2025-06-26T08:39:59.980605063+02:00"},"size":252546120,"permissions":{"type":"Unix permissions","owner":"root","group":"root","mask":{"user":{"readable":true,"writable":true,"executable":true},"group":{"readable":true,"writable":false,"executable":true},"world":{"readable":true,"writable":false,"executable":true}}}},"parent_info":{"pid":9011,"exe":"/usr/lib/chromium/chromium","command":"/usr/lib/chromium/chromium"},"tree":["/usr/lib/chromium/chromium","/usr/lib/chromium/chromium"],"created":"2025-07-01T12:00:05+02:00","session":"","listen_ports":null,"connections":[]},"score":0,"reasons":null,"reason_count":0,"context":null,"issues":[{"affected":"/subject/sections","category":"truncated","description":"Removed some sections from process memory (originally 638)"}],"log_version":"v3.0.0"}`
var assessmentObj Assessment
if err := json.Unmarshal([]byte(assessment), &assessmentObj); err != nil {
t.Fatalf("Failed to unmarshal assessment: %v", err)
}
}
2 changes: 1 addition & 1 deletion thorlog/v3/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package thorlog

import "github.com/NextronSystems/jsonlog"

// Issue describes a problem that occurred during the analysis of a scan target like a file or process.
// Issue describes a problem that occurred during the assessment of a scan target like a file or process.
// Often this will be an issue with displaying the results,
// e.g. the results may be truncated due to size limitations.
type Issue struct {
Expand Down
8 changes: 4 additions & 4 deletions thorlog/v3/reason.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/NextronSystems/jsonlog"
)

// Reason describes a match of a single Signature on a ReportableObject.
// Reason describes a match of a single Signature on a AssessableObject.
type Reason struct {
jsonlog.ObjectHeader

Expand Down Expand Up @@ -38,11 +38,11 @@ func init() {
type Signature struct {
// Score is a metric that combines severity and certainty for this signature.
//
// It is related to the Finding.Score, which is derived from the scores of all
// It is related to the Assessment.Score, which is derived from the scores of all
// signatures that matched; however, signature scores are not limited to the
// 0 to 100 interval of finding scores, but may also be negative to indicate
// 0 to 100 interval of assessment scores, but may also be negative to indicate
// a likely false positive (which results in a score reduction on any related
// finding).
// assessment).
Score int64 `json:"score" textlog:"subscore"`
// Ref contains references (usually as links) for further information about
// the threat that is detected by this signature.
Expand Down