I've decided to start dumping my knowledge of ACS for posterity's sake. My first installment is here, and it's an excerpt from an external email I put together which describes how event transformation works on ACS.
Transformation is performed on the agent (using instructions provided at connect time by the collector) and on the collector. Transformation instructions are all stored on the collector in a file called EventSchema.xml which is in the AdtServer directory (%windir%\system32\security\adtserver). This file is pointed to in the collector’s registry and is read during startup of the collector service; failure to successfully read and parse this file at startup is a fatal error for the collector (the debug log will complain about parsing).
The collector reads EventSchema.xml and builds in-memory binary tables of event transformation instructions and event string types by OS version/event log/event source.
The collector (as explained elsewhere) also reads AcsConfig.xml to get its persistent state and configuration for all known agents, to know what logs/sources to collect for each agent/agent group, etc. This is all read into in-memory state for each agent.
At connect time, the agent sends version information- what the OS and agent version and service pack are, etc. The collector first looks in its in-memory agent state to see what configuration applies to the agent. Then it looks in its transformation tables and extracts the appropriate version-specific transformation instructions for the events that the collector is configured to collect from that agent. Then it packages these instructions and sends them to the agent.
The agent starts reading events, transforming them according to its instructions from the collector, and sending the transformed events to the collector. The collector finishes the transformation, services real-time subscriptions and loads the events into the database as appropriate.
If the agent encounters an event that is it configured to send (by log/source) but does not have transformation instructions for, then it simply builds a copy the event string for string and sends the copy of the event to the collector as an “unschematized” event. The collector will handle this event without problems but will not extract non-header user fields (no primary/client/target user fields) and will not add string type information.
I’ll take Windows Server 2003 (build 3790), Event Log: Security, Event Source: Security, Event ID: 644 as an example.
Here’s the WS03 schema for 644 (excerpt from %systemroot%\system32\security\adtserver\EventSchema.xml in the path “Schema\Log[@Name=’Security’\Source[@Name=’Security’]\Version[@MinBuild=’3790’]\Event[@SourceId=’644’]”).
<Event SourceId="644" SourceName="SE_AUDITID_ACCOUNT_AUTO_LOCKED">
<Call Name="AppendString" Param1="1" Param2="0" />
<Call Name="AppendString" Param1="3" Param2="0" />
<Call Name="AppendString" Param1="2" Param2="0" />
<Call Name="AppendString" Param1="4" Param2="0" />
<Call Name="AppendString" Param1="5" Param2="0" />
<Call Name="AppendString" Param1="6" Param2="0" />
<Call Name="AppendSidFromNames" Param1="4" Param2="5" />
<Call Name="AppendNamesFromSid" Param1="3" Param2="0" />
<Param TypeName="typeUserDn" />
<Param TypeName="typeComputerName" />
<Param TypeName="typeTargetSid" />
<Param TypeName="typeClientUser" />
<Param TypeName="typeClientDomain" />
<Param TypeName="typeClientLogonId" />
<Param TypeName="typeClientSid" />
<Param TypeName="typeTargetUser" />
<Param TypeName="typeTargetDomain" />
</Event>
The instructions are all applied in order. “Call” instructions are executed agent-side; “Param” instructions are executed server-side.
These instructions can be translated as:
· Take string 1 from the original event and make it string 1 in the new event. It is of type “typeUserDn”.
· Take string 3 from the original event and make it string 2 in the new event. It is of type “typeComputerName”. Note that we are doing reordering here by appending original string #3 before original string #2. Nifty, eh?
· Take string 2 from the original event and make it string 3 in the new event. It is of type “typeTargetSid”.
· Take string 4 from the original event and make it string 4 in the new event. It is of type “typeClientUser”.
· Take string 5 from the original event and make it string 5 in the new event. It is of type “typeClientDomain”.
· Take string 6 from the original event and make it string 6 in the new event. It is of type “typeClientLogonId”.
· Take string 4 from the original event and treat is as a user name, and take string 5 from the original event and treat it as a domain name, look up the associated SID and make it string 7 in the new event. The new string is of type “typeClientSid”.
· Take string 3 from the new event, treat it as a SID, look up the user/domain name associated with it and append the user name as string 8 to the new event and the domain name as string 9 to the new event. String 8 is of type “typeTargetUser” and String 9 is of type “typeTargetDomain”.
See the reordering? Now here is an instance of the event with the original event data. If you’re not familiar with the XML, it’s the XML output of Crimson, the new eventlog service introduced in Vista/WS08, but this is a WS03 [pre-Crimson] machine; we're looking at a saved event log (evt) file.
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Security" />
<EventID Qualifiers="0">644</EventID>
<Level>0</Level>
<Task>7</Task>
<Keywords>0xa0000000000000</Keywords>
<TimeCreated SystemTime="2007-12-17T15:50:14.000Z" />
<EventRecordID>28003981</EventRecordID>
<Channel>C:\Users\ericf\AppData\Local\Temp\SERVER34_SecEvts.evt</Channel>
<Computer>SERVER34</Computer>
<Security UserID="S-1-5-18" />
</System>
<EventData>
<Data>user09</Data> // String 1 – user name
<Data>SERVER34</Data> // String 2 – looks like a machine name, confirmed by string 4
<Data>%{S-1-5-21-5998314728-109421381-169156293-611111}</Data> // String 3 – definitely a SID
<Data>SERVER34$</Data> // String 4 – definitely an account name (machine account)
<Data>CONTOSO</Data> // String 5 – looks like a domain name
<Data>(0x0,0x3E7)</Data> // String 6 – definitely a logon ID
<Data>-</Data> // String 7 – empty null string at the end of the event (ignored by ACS)
</EventData>
</Event>
When the event arrives at the collector, type information is applied, and then the user fields (typePrimary*, typeClient*, typeTarget*) are extracted from the string data section and the strings that are left are re-numbered starting at 1 (no reordering occurs).
Here’s a chart of what the event looks like at the various points in the system. The changes at each step are shown in red.
|
Original Event in Event Log |
Client-Side Transformation at Agent |
Server-Side Normalization (WMI/SQL output) |
|
Field |
Content Description (implicit) |
Field |
Content Description (implicit) |
Field |
Content Description (explicit) |
|
|
|
Client User |
|
Client User |
typeClientUser |
|
|
|
Client Domain |
|
Client Domain |
typeClientDomain |
|
|
|
Client Sid |
|
Client Sid |
typeClientSid |
|
|
|
Client Login Id |
|
Client Login Id |
typeClientLogonId |
|
|
|
Target User |
|
Target User |
typeTargetUser |
|
|
|
Target Domain |
|
Target Domain |
typeTargetDomain |
|
|
|
Target Sid |
|
Target Sid |
typeTargetSid |
|
String01 |
typeUserDn |
String01 |
typeUserDn |
String01 |
typeUserDn |
|
String02 |
typeTargetSid |
String02 |
typeComputerName |
String02 |
typeComputerName |
|
String03 |
typeComputerName |
String03 |
typeTargetSid |
String03 |
|
|
String04 |
typeClientUser |
String04 |
typeClientUser |
String04 |
|
|
String05 |
typeClientDomain |
String05 |
typeClientDomain |
String05 |
|
|
String06 |
typeClientLogonId |
String06 |
typeClientLogonId |
String06 |
|
|
String07 |
|
String07 |
typeClientSid |
String07 |
|
|
String08 |
|
String08 |
typeTargetUser |
String08 |
|
|
String09 |
|
String09 |
typeTargetDomain |
String09 |
|
To finish off a description of transformation, there are 7 transformation functions, each of which can optionally take 2 integers as parameters. Note that there is no “destination event” field specifier; all references are only to the original event. That’s because when constructing the destination event, any data added to the event is always appended- it is constructed from beginning to end- so the implicit destination field is “at the end of the event as it is now”.
|
Function |
Parameter 1 |
Parameter 2 |
Description |
|
AppendString |
Reference to a string parameter in the source event in the event log |
Unused |
Appends the referenced string to the event which will be sent to the collector |
|
AppendStringFromTable< |