Skip to content

Commit

Permalink
Wel details (#159)
Browse files Browse the repository at this point in the history
* Implement security channel message parsing
  • Loading branch information
djaglowski authored Oct 7, 2020
1 parent 1db5ac4 commit 7871461
Show file tree
Hide file tree
Showing 68 changed files with 983 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.12.5] - 2020-10-07
### Added
- `windows_eventlog_input` can now parse messages from the Security channel.

## [0.12.4] - 2020-10-07
### Fixed
- Router outputs were not namespaced correctly
Expand Down
187 changes: 187 additions & 0 deletions operator/builtin/input/windows/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package windows

import (
"strings"
)

func parseSecurity(message string) (string, map[string]interface{}) {

subject, details := message, map[string]interface{}{}

mp := newMessageProcessor(message)

// First line is expected to be the first return value
l := mp.next()
switch l.t {
case valueType:
subject = l.v
case keyType:
subject = l.k
default:
return message, nil
}

moreInfo := []string{}

for mp.hasNext() {
l = mp.next()
switch l.t {
case valueType:
moreInfo = append(moreInfo, l.v)
case keyType:
if !mp.hasNextIndented(l.i + 1) {
// standalone key/value pair with empty value
details[l.k] = "-"
continue
}
details[l.k] = mp.consumeSubsection(l.i + 1)
case pairType:
if !mp.hasNextIndented(l.i + 1) {
// standalone key/value pair
details[l.k] = l.v
continue
}
// value was first in a list
details[l.k] = append([]string{l.v}, mp.consumeSublist(l.i+1)...)
}
}

if len(moreInfo) > 0 {
details["Additional Context"] = moreInfo
}

return subject, details
}

func (mp *messageProcessor) consumeSubsection(depth int) map[string]interface{} {
sub := map[string]interface{}{}
for mp.hasNext() {
l := mp.next()
switch l.t {
case emptyType:
return sub
case pairType:
sub[l.k] = l.v
case keyType:
if !mp.hasNextIndented(depth + 1) {
// standalone key/value pair with missing value
sub[l.k] = "-"
continue
}
sub[l.k] = mp.consumeSublist(depth + 1)
}
}
return sub
}

func (mp *messageProcessor) consumeSublist(depth int) []string {
sublist := []string{}
for mp.hasNext() {
if !mp.hasNextIndented(depth) {
return sublist
}
l := mp.next()
switch l.t {
case valueType:
sublist = append(sublist, l.v)
case keyType: // not expected, but handle
sublist = append(sublist, l.k)
}
}
return sublist
}

type messageProcessor struct {
lines []*parsedLine
ptr int
}

type parsedLine struct {
t lineType
i int
k string
v string
}

type lineType int

const (
emptyType lineType = iota
keyType
valueType
pairType
)

func newMessageProcessor(message string) *messageProcessor {
unparsedLines := strings.Split(strings.TrimSpace(message), "\n")
parsedLines := make([]*parsedLine, len(unparsedLines))
for i, unparsedLine := range unparsedLines {
parsedLines[i] = parse(unparsedLine)
}
return &messageProcessor{lines: parsedLines}
}

func parse(line string) *parsedLine {
i := countIndent(line)
l := strings.TrimSpace(line)
if l == "" {
return &parsedLine{t: emptyType, i: i}
}

if strings.Contains(l, ":\t") {
k, v := parseKeyValue(l)
return &parsedLine{t: pairType, i: i, k: k, v: v}
}

if strings.HasSuffix(l, ":") {
return &parsedLine{t: keyType, i: i, k: l[:len(l)-1]}
}

return &parsedLine{t: valueType, i: i, v: l}
}

// return next line and increment position
func (mp *messageProcessor) next() *parsedLine {
defer mp.step()
return mp.lines[mp.ptr]
}

// return next line but do not increment position
func (mp *messageProcessor) peek() *parsedLine {
return mp.lines[mp.ptr]
}

// just increment position
func (mp *messageProcessor) step() {
mp.ptr++
}

func (mp *messageProcessor) hasNext() bool {
return mp.ptr < len(mp.lines)
}

func (mp *messageProcessor) hasNextIndented(minDepth int) bool {
if !mp.hasNext() || mp.ptr == 0 {
return false
}

l := mp.peek()
if l.t == emptyType {
return false
}

return l.i >= minDepth
}

func countIndent(line string) int {
i := 1
for pre := strings.Repeat("\t", i); strings.HasPrefix(line, pre); pre = strings.Repeat("\t", i) {
i++
}
return i - 1
}

func parseKeyValue(line string) (string, string) {
kv := strings.SplitN(line, ":\t", 2)
return strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
}
73 changes: 73 additions & 0 deletions operator/builtin/input/windows/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package windows

import (
"encoding/json"
"io/ioutil"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestParseSecurity(t *testing.T) {

testCases := []string{
"account_name_changed",
"audit_settings_changed",
"audit_success",
"credential_validate_attempt",
"domain_policy_changed",
"driver_started",
"event_processing",
"local_group_changed",
"logon",
"object_added",
"per_user_audit_policy_table_created",
"query_blank_password",
"service_shutdown",
"service_started",
"special_logon",
"time_change",
"user_account_changed",
"user_account_created",
"user_account_enabled",
"user_added_to_global_group",
"user_password_reset_attempt",
}

for _, tc := range testCases {
t.Run(tc, func(t *testing.T) {

testDir := filepath.Join("testdata", "security", tc)
messageBytes, err := ioutil.ReadFile(filepath.Join(testDir, "message.in"))
require.NoError(t, err, "problem reading input file")

message, details := parseSecurity(string(messageBytes))

// initTestResult(testDir, message, details)

expectedMessageBytes, err := ioutil.ReadFile(filepath.Join(testDir, "message.out"))
require.NoError(t, err, "problem reading expected message")
expectedMessage := string(expectedMessageBytes)

expectedDetailsBytes, err := ioutil.ReadFile(filepath.Join(testDir, "details.out"))
require.NoError(t, err, "problem reading expected details")

// This is a little silly, but if we rely on unmarshaling
// then []string gets converted to []interface{} and the comparison fails
detailBytes, err := json.Marshal(details)
require.NoError(t, err, "problem processing details result")

require.Equal(t, expectedMessage, message)
require.JSONEq(t, string(expectedDetailsBytes), string(detailBytes))
})
}
}

// Use this to initialize test results from a WEL security message
// make sure to validate manually!
func initTestResult(testDir, message string, details map[string]interface{}) {
ioutil.WriteFile(filepath.Join(testDir, "message.out"), []byte(message), 0644)
bytes, _ := json.MarshalIndent(details, "", " ")
ioutil.WriteFile(filepath.Join(testDir, "details.out"), bytes, 0644)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Additional Information": {
"Privileges": "-"
},
"Subject": {
"Account Domain": "WORKGROUP",
"Account Name": "WIN-322E2C550UP$",
"Logon ID": "0x3E7",
"Security ID": "SYSTEM"
},
"Target Account": {
"Account Domain": "Builtin",
"New Account Name": "Power Users",
"Old Account Name": "Power Users",
"Security ID": "BUILTIN\\Power Users"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
The name of an account was changed:

Subject:
Security ID: SYSTEM
Account Name: WIN-322E2C550UP$
Account Domain: WORKGROUP
Logon ID: 0x3E7

Target Account:
Security ID: BUILTIN\Power Users
Account Domain: Builtin
Old Account Name: Power Users
New Account Name: Power Users

Additional Information:
Privileges: -
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The name of an account was changed
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"Auditing Settings": {
"New Security Descriptor": "S:ARAI(AU;SAFA;DCLCRPCRSDWDWO;;;WD)",
"Original Security Descriptor": "-"
},
"Object": {
"Handle ID": "0x72c",
"Object Name": "C:\\Windows\\Temp\\winre\\ExtractedFromWim",
"Object Server": "Security",
"Object Type": "File"
},
"Process Information": {
"Process ID": "0x2ec",
"Process Name": "C:\\Windows\\System32\\oobe\\Setup.exe"
},
"Subject": {
"Account Domain": "WORKGROUP",
"Account Name": "WIN-PS00R22J635$",
"Logon ID": "0x3E7",
"Security ID": "SYSTEM"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Auditing settings on object were changed.

Subject:
Security ID: SYSTEM
Account Name: WIN-PS00R22J635$
Account Domain: WORKGROUP
Logon ID: 0x3E7

Object:
Object Server: Security
Object Type: File
Object Name: C:\Windows\Temp\winre\ExtractedFromWim
Handle ID: 0x72c

Process Information:
Process ID: 0x2ec
Process Name: C:\Windows\System32\oobe\Setup.exe

Auditing Settings:
Original Security Descriptor:
New Security Descriptor: S:ARAI(AU;SAFA;DCLCRPCRSDWDWO;;;WD)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Auditing settings on object were changed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"Additional Context": [
"This event is logged when LSASS.EXE starts and the auditing subsystem is initialized."
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Windows is starting up.

This event is logged when LSASS.EXE starts and the auditing subsystem is initialized.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Windows is starting up.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Authentication Package": "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0",
"Error Code": "0x0",
"Logon Account": "Someone",
"Source Workstation": "WIN-322E2C550UP"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The computer attempted to validate the credentials for an account.

Authentication Package: MICROSOFT_AUTHENTICATION_PACKAGE_V1_0
Logon Account: Someone
Source Workstation: WIN-322E2C550UP
Error Code: 0x0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The computer attempted to validate the credentials for an account.
Loading

0 comments on commit 7871461

Please sign in to comment.