Microsoft NPS Server Logfile Parser

Microsoft NPS Server creates logs via EventLog and logfiles.
Capturing the Event Logs is pretty straight forward with a tool like NXLog, but parsing the Logfile is more complicated, so I want to share how I did it. I’m not using extractors because we use Graylog Forwarders in our environment and you can’t use them together. If you’re using NXLogEE you can use the nps extension and skip this guide, otherwise keep reading.

first step is to setup the NPS logfile format.
grafik
There you have to click on Logfile Properties and change the Format to ODBC(Legacy). This way the logfiles will be created in csv format and be compliant to the documentation here: Interpret NPS Database Format Log Files | Microsoft Learn

Next step is to send the Logfile to your Graylog environment. Create an index, input and stream for your NPS logs the way you do it normally in your environment. I’m using a seperate input, stream and index for the NPS logs but thats not mandatory.
To send the logfiles to graylog I’m using NXLog.
Here is the nxlog.conf

Panic Soft
#####################################################################
##Default Definitions################################################
#####################################################################
define ROOT     C:\Program Files\nxlog
define CERTDIR  %ROOT%\cert
define CONFDIR  %ROOT%\conf
define LOGDIR   %ROOT%\data
define LOGFILE  %LOGDIR%\nxlog.log
LogFile %LOGFILE%
Moduledir %ROOT%\modules
CacheDir  %ROOT%\data
Pidfile   %ROOT%\data\nxlog.pid
SpoolDir  %ROOT%\data
#####################################################################
##Custom Definitions#################################################
#####################################################################
##Destination Server:
define DESTSRV 10.100.248.171
##Destination Port:
define DESTPORT 3510
define DESTPORTNAP 3515
#####################################################################
##Extensions#########################################################
#####################################################################
##Logrotation
<Extension _fileop>
    Module      xm_fileop
    # Check the size of our log file hourly, rotate if larger than 5MB
    <Schedule>
        Every   1 hour
        Exec    if (file_exists('%LOGFILE%') and \
                   (file_size('%LOGFILE%') >= 5M)) \
                    file_cycle('%LOGFILE%', 8);
    </Schedule>
    # Rotate our log file every week on Sunday at midnight
    <Schedule>
        When    @weekly
        Exec    if file_exists('%LOGFILE%') file_cycle('%LOGFILE%', 8);
    </Schedule>
</Extension>
##GELF Extension Module
<Extension _gelf>
    Module      xm_gelf
</Extension>
#####################################################################
##Inputs#############################################################
#####################################################################
<Input EventLogs>
    Module            im_msvistalog
    <QueryXML>
        <QueryList>
            <Query Id='0'>
                <Select Path='Application'>*</Select>
                <Select Path='Security'>*</Select>
                <Select Path='System'>*</Select>
                <Select Path='Setup'>*</Select>
            </Query>
        </QueryList>
    </QueryXML>
</Input>
<Input z000nap01_logfiles>
	Module 			im_file
	#File 			'C:\\Program Files\\Microsoft Configuration Manager\\Logs\\*.log'
	File			'C:\\Windows\\System32\\LogFiles\\IN*.log'
	SavePos 		TRUE
	ReadFromLast 	TRUE
	PollInterval 	1
	Exec $Message = $raw_event; $SyslogFacilityValue = 22;
	Exec $logname = file_basename(file_name());
</Input>
#####################################################################
##Outputs############################################################
#####################################################################
<Output graylog>
	Module 		om_tcp
	Host		%DESTSRV%
	Port		%DESTPORT%
	OutputType	GELF_TCP
</Output>
<Output graylogNAP>
	Module 		om_tcp
	Host		%DESTSRV%
	Port		%DESTPORTNAP%
	OutputType	GELF_TCP
</Output>
#####################################################################
##Routes#############################################################
#####################################################################
<Route graylog_route>
	Path		EventLogs => graylog
</Route>
<Route graylog_route>
	Path		z000nap01_logfiles => graylogNAP
</Route>

DESTPORT variable is the port for the windows event logs. Use the port you’re using for windows events input in your environment.
DESTPORTNAP variable is the port for the nps logfile. Use the port for your newly created input.

Now your NPS server should send the logfiles to graylog, but they are not split into fields. to change that we need a pipeline rule and a pipeline.

Create a new rule under system/pipeline/manage rules/create rule.
Here is the parser:

rule "MS NPS Logfile Parser"
// Parses Logfiles created via Microsoft NPS Server. 
// Logfile format has to be ODBC(Legacy)
when 
    has_field("message")
then
    let m = to_string($message.full_message);
    //add some empty csv entries for shorter logs that are not compliant to the
    //MS documentation and a character that does not get parsed to prevent out 
    //of bound errors
    let m = regex_replace(",$", to_string(m), ",,,x");
    let m = split(",", to_string(m));

    set_field("nps_computername", m[0]);
    set_field("nps_servicename", m[1]);
    set_field("nps_date", m[2]);
    set_field("nps_time", m[3]);
    set_field("nps_packettype", m[4]);
    set_field("nps_username", m[5]);
    set_field("nps_fqdn", m[6]);
    set_field("nps_calledstationID", m[7]);
    set_field("nps_callingstationID", m[8]);
    set_field("nps_callbacknumber", m[9]);
    set_field("nps_framedIP", m[10]);
    set_field("nps_NASidentifier", m[11]);
    set_field("nps_NASIP", m[12]);
    set_field("nps_NASport", m[13]);
    set_field("nps_clientvendor", m[14]);
    set_field("nps_clientIP", m[15]);
    set_field("nps_clientfriendlyname", m[16]);
    set_field("nps_eventtimestamp", m[17]);
    set_field("nps_portlimit", m[18]);
    set_field("nps_NASporttype", m[19]);
    set_field("nps_connectinfo", m[20]);
    set_field("nps_framedprotocol", m[21]);
    set_field("nps_servicetype", m[22]);
    set_field("nps_authenticationtype", m[23]);
    set_field("nps_policyname", m[24]);
    set_field("nps_reasoncode", m[25]);
    set_field("nps_class", m[26]); 
    set_field("nps_sessiontimeout", m[27]);
    set_field("nps_idletimeout", m[28]);
    set_field("nps_terminationaction", m[29]);
    set_field("nps_EAPfriendlyname", m[30]);
    set_field("nps_acctstatustype", m[31]);
    set_field("nps_acctdelaytime", m[32]);
    set_field("nps_acctinputoctets", m[33]);
    set_field("nps_acctoutputoctets", m[34]);
    set_field("nps_acctsessionID", m[35]);
    set_field("nps_acctauthentic", m[36]);
    set_field("nps_acctsessiontime", m[37]);
    set_field("nps_acctinputpackets", m[38]);
    set_field("nps_acctoutputpackets", m[39]);
    set_field("nps_acctterminatecause", m[40]);
    set_field("nps_acctmultissnID", m[41]);
    set_field("nps_acctlinkcount", m[42]); 
    set_field("nps_acctinteriminterval", m[43]);
    set_field("nps_tunneltype", m[44]);
    set_field("nps_tunnelmediumtype", m[45]);
    set_field("nps_tunnelclientendpt", m[46]);
    set_field("nps_tunnelserverendpt", m[47]);
    set_field("nps_accttunnelconn", m[48]); 
    set_field("nps_tunnelpvtgroupID", m[49]);
    set_field("nps_tunnelassignmentID", m[50]);
    set_field("nps_tunnelpreference", m[51]);
    set_field("nps_MSacctauthtype", m[52]);
    set_field("nps_MSacctEAPtype", m[53]);
    set_field("nps_MSRASversion", m[54]);
    set_field("nps_MSRASVendor", m[55]);
    set_field("nps_MSCHAPerror", m[56]);
    set_field("nps_MSCHAPdomain", m[57]);
    set_field("nps_MSMPPEencryptiontypes", m[58]);
    set_field("nps_MSMPPEencryptionpolicy", m[59]);
    set_field("nps_proxypolicyname", m[60]);
    set_field("nps_providertype", m[61]);
    set_field("nps_providername", m[62]);
    set_field("nps_remoteserveraddress", m[63]);
    set_field("nps_MSRASclientname", m[64]);
    set_field("nps_MSRASclientversion", m[65]);
end

Save the rule, create a new pipeline under manage pipelines/add new pipeline, connect the stream that contains the NPS logfiles with “edit connection”, edit Stage 0 and add your MS NPS Logfile Parser.

@fsv

Pretty awesome man, thank for sharing.

Getting the following error for ingestion.

gl2_processing_error
Error evaluating action for rule <nps radius logfile parser/6476c46cd8511407b37dcf6f> (pipeline <nps radius logfiles/6476c4a4d8511407b37dcfe6>) - In call to function ‘set_field’ at 15:4 an exception was thrown: index (1) must be less than size (1)

In case this helps anyone in the future, I ran into the same error message as @cyr0nk0r and ended up changing the line

    let m = to_string($message.full_message);

to

    let m = to_string($message.message);

because when looking at the available fields in graylog there wasn’t a “full_message” field but there was a “message” one. I’m also using filebeat to import my logs via sidecar instead of nxlog which may be the difference between my environment and @fsv’s.

Ah nice find.
we are using Graylog Enterprise with Illuminate. Illuminate normalizes fields between different inputs, so I guess thats why they changed the default message field to full_message in our environment.