glossy
Version:
Syslog parser and producer
521 lines (448 loc) • 17.5 kB
JavaScript
/*
* Glossy Parser - Parse incoming syslog messages
*
* Copyright Squeeks <privacymyass@gmail.com>.
* This is free software licensed under the MIT License -
* see the LICENSE file that should be included with this package.
*/
/*
* These values replace the integers in message that define the facility.
*/
var FacilityIndex = [
'kern', // kernel messages
'user', // user-level messages
'mail', // mail system
'daemon', // system daemons
'auth', // security/authorization messages
'syslog', // messages generated internally by syslogd
'lpr', // line printer subsystem
'news', // network news subsystem
'uucp', // UUCP subsystem
'clock', // clock daemon
'sec', // security/authorization messages
'ftp', // FTP daemon
'ntp', // NTP subsystem
'audit', // log audit
'alert', // log alert
'clock', // clock daemon (note 2)
'local0', // local use 0 (local0)
'local1', // local use 1 (local1)
'local2', // local use 2 (local2)
'local3', // local use 3 (local3)
'local4', // local use 4 (local4)
'local5', // local use 5 (local5)
'local6', // local use 6 (local6)
'local7' // local use 7 (local7)
];
// Note 1 - Various operating systems have been found to utilize
// Facilities 4, 10, 13 and 14 for security/authorization,
// audit, and alert messages which seem to be similar.
// Note 2 - Various operating systems have been found to utilize
// both Facilities 9 and 15 for clock (cron/at) messages.
/*
* These values replace the integers in message that define the severity.
*/
var SeverityIndex = [
'emerg', // Emergency: system is unusable
'alert', // Alert: action must be taken immediately
'crit', // Critical: critical conditions
'err', // Error: error conditions
'warn', // Warning: warning conditions
'notice', // Notice: normal but significant condition
'info', // Informational: informational messages
'debug' // Debug: debug-level messages
];
/*
* Defines the range matching BSD style months to integers.
*/
var BSDDateIndex = {
'Jan': 0,
'Feb': 1,
'Mar': 2,
'Apr': 3,
'May': 4,
'Jun': 5,
'Jul': 6,
'Aug': 7,
'Sep': 8,
'Oct': 9,
'Nov': 10,
'Dec': 11
};
// These values match the hasing algorithm values as defined in RFC 5848
var signedBlockValues = {
// Section 4.2.1
hashAlgorithm: [
null,
'SHA1',
'SHA256'
],
// Section 5.2.1
keyBlobType: {
'C': 'PKIX Certificate',
'P': 'OpenPGP KeyID',
'K': 'Public Key',
'N': 'No key information',
'U': 'Unknown'
}
};
var GlossyParser = function() {};
/*
* Parse the raw message received.
*
* @param {String/Buffer} rawMessage Raw message received from socket
* @param {Function} callback Callback to run after parse is complete
* @return {Object} map containing all successfully parsed data.
*/
GlossyParser.prototype.parse = function(rawMessage, callback) {
// Are you node.js? Is this a Buffer?
if(typeof Buffer == 'function' && Buffer.isBuffer(rawMessage)) {
rawMessage = rawMessage.toString('utf8', 0);
} else if(typeof rawMessage != 'string') {
return rawMessage;
}
// Always return the original message
var parsedMessage = {
originalMessage: rawMessage
};
var segments = rawMessage.split(' ');
if(segments.length < 2) return parsedMessage;
var priKeys = this.decodePri(segments[0]);
if(priKeys) {
for (var key in priKeys) parsedMessage[key] = priKeys[key];
}
var timeStamp;
//TODO Could our detection between 3164/5424 be improved?
if(segments[0].match(/^(<\d+>\d)$/)) {
segments.shift(); // Shift the prival off
timeStamp = segments.shift();
parsedMessage.type = 'RFC5424';
parsedMessage.time = this.parseTimeStamp(timeStamp);
parsedMessage.host = this.decideValue(segments.shift());
parsedMessage.appName = this.decideValue(segments.shift());
parsedMessage.pid = this.decideValue(segments.shift());
parsedMessage.msgID = this.decideValue(segments.shift());
if(segments[0] !== '-') {
var spliceMarker = 0;
for (i = segments.length -1; i > -1; i--) {
if(segments[i].substr(-1) === ']'){
spliceMarker = i;
spliceMarker++;
break;
}
}
if(spliceMarker !== 0) {
var sd = segments.splice(0, spliceMarker).join(' ');
parsedMessage.structuredData = this.parseStructure(sd);
if(parsedMessage.structuredData.ssign) {
parsedMessage.structuredData.signedBlock =
this.parseSignedBlock(parsedMessage.structuredData);
} else if(parsedMessage.structuredData['ssign-cert']) {
parsedMessage.structuredData.signedBlock =
this.parseSignedCertificate(parsedMessage.structuredData);
}
}
} else {
segments.shift(); // Shift the SD marker off
}
parsedMessage.message = segments.join(' ');
} else if (segments[0].match(/^(<\d+>\d+:)$/)) {
parsedMessage.type = 'RFC3164';
timeStamp = segments.splice(0,1).join(' ').replace(/^(<\d+>)/,'');
parsedMessage.time = this.parseBsdTime(timeStamp);
parsedMessage.message = segments.join(' ');
} else if(segments[0].match(/^(<\d+>\w+)/)) {
parsedMessage.type = 'RFC3164';
if (segments[1] === '') segments.splice(1,1);
timeStamp = segments.splice(0,3).join(' ').replace(/^(<\d+>)/,'');
parsedMessage.time = this.parseBsdTime(timeStamp);
parsedMessage.host = segments.shift();
parsedMessage.message = segments.join(' ');
}
if(callback) {
callback(parsedMessage);
} else {
return parsedMessage;
}
};
/*
* RFC5424 messages are supposed to specify '-' as the null value
* @param {String} a section from an RFC5424 message
* @return {Boolean/String} null if string is entirely '-', or the original value
*/
GlossyParser.prototype.decideValue = function(value) {
return value === '-' ? null : value;
};
/*
* Parses the PRI value from the start of message
*
* @param {String} message Supplied raw primary value and version
* @return {Object} Returns object containing Facility, Severity and Version
* if correctly parsed, empty values on failure.
*/
GlossyParser.prototype.decodePri = function(message) {
if(typeof message != 'string') return;
var privalMatch = message.match(/^<(\d+)>/);
if(!privalMatch) return false;
var returnVal = {
prival: parseInt(privalMatch[1], 10)
};
if(privalMatch[2]) returnVal.versio = parseInt(privalMatch[2], 10);
if(returnVal.prival && returnVal.prival >= 0 && returnVal.prival <= 191) {
returnVal.facilityID = parseInt(returnVal.prival / 8, 10);
returnVal.severityID = returnVal.prival - (returnVal.facilityID * 8);
if(returnVal.facilityID < 24 && returnVal.severityID < 8) {
returnVal.facility = FacilityIndex[returnVal.facilityID];
returnVal.severity = SeverityIndex[returnVal.severityID];
}
} else if(returnVal.prival >= 191) {
return false;
}
return returnVal;
};
/*
* Attempts to parse a given timestamp
* @param {String} timeStamp Supplied timestamp, should only be the timestamp,
* not the entire message
* @return {Object} Date object on success
*/
GlossyParser.prototype.parseTimeStamp = function(timeStamp) {
if(typeof timeStamp != 'string') return;
var parsedTime;
parsedTime = this.parse8601(timeStamp);
if(parsedTime) return parsedTime;
parsedTime = this.parseRfc3339(timeStamp);
if(parsedTime) return parsedTime;
parsedTime = this.parseBsdTime(timeStamp);
if(parsedTime) return parsedTime;
return parsedTime;
};
/*
* Parse RFC3339 style timestamps
* @param {String} timeStamp
* @return {Date/false} Timestamp, if parsed correctly
* @see http://blog.toppingdesign.com/2009/08/13/fast-rfc-3339-date-processing-in-javascript/
*/
GlossyParser.prototype.parseRfc3339 = function(timeStamp){
var utcOffset, offsetSplitChar, offsetString,
offsetMultiplier = 1,
dateTime = timeStamp.split("T");
if(dateTime.length < 2) return false;
var date = dateTime[0].split("-"),
time = dateTime[1].split(":"),
offsetField = time[time.length - 1];
offsetFieldIdentifier = offsetField.charAt(offsetField.length - 1);
if (offsetFieldIdentifier === "Z") {
utcOffset = 0;
time[time.length - 1] = offsetField.substr(0, offsetField.length - 2);
} else {
if (offsetField[offsetField.length - 1].indexOf("+") != -1) {
offsetSplitChar = "+";
offsetMultiplier = 1;
} else {
offsetSplitChar = "-";
offsetMultiplier = -1;
}
offsetString = offsetField.split(offsetSplitChar);
if(offsetString.length < 2) return false;
time[(time.length - 1)] = offsetString[0];
offsetString = offsetString[1].split(":");
utcOffset = (offsetString[0] * 60) + offsetString[1];
utcOffset = utcOffset * 60 * 1000;
}
var parsedTime = new Date(Date.UTC(date[0], date[1] - 1, date[2], time[0], time[1], time[2]) + (utcOffset * offsetMultiplier ));
return parsedTime;
};
/*
* Parse "BSD style" timestamps, as defined in RFC3164
* @param {String} timeStamp
* @return {Date/false} Timestamp, if parsed correctly
*/
GlossyParser.prototype.parseBsdTime = function(timeStamp) {
var parsedTime;
var d = timeStamp.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})/);
if(d) {
// Years are absent from the specification, use this year
currDate = new Date();
parsedTime = new Date(
currDate.getUTCFullYear(),
BSDDateIndex[ d[1] ],
d[2],
d[3],
d[4],
d[5]);
}
return parsedTime;
};
/*
* Parse ISO 8601 timestamps
* @param {String} timeStamp
* @return {Object/false} Timestamp, if successfully parsed
*/
GlossyParser.prototype.parse8601 = function(timeStamp) {
var parsedTime = new Date(Date.parse(timeStamp));
if(parsedTime.toString() === 'Invalid Date') return; //FIXME not the best
return parsedTime;
};
/*
* Parse the structured data out of RFC5424 messages
* @param {String} msg The STRUCTURED-DATA section
* @return {Object} sdStructure parsed structure
*/
GlossyParser.prototype.parseStructure = function(msg) {
var sdStructure = { };
var state = 0,
ignore = false,
sdId = '',
sdParam = '',
sdValue = '';
/*
* Build the structure using a horrible FSM.
* The states we cycle are as following:
* 0 1 2 34 20
* [sdID sdParam="sdValue"]
*/
for(var i = 0; i < msg.length; i++) {
var c = msg[i];
switch(state) {
case 0: // SD-ELEMENT
state = (c === '[') ? 1 : 0;
break;
case 1: // SD-ID
if(c != ' ') {
sdId += c;
} else {
sdStructure[sdId] = {};
state = 2;
}
break;
case 2: // SD-PARAM
if(c === '=') {
sdStructure[sdId][sdParam] = '';
state = 3;
} else if(c === ']') {
sdId = '';
state = 0;
} else if(c != ' '){
sdParam += c;
}
break;
case 3: // SD-PARAM/SD-VALUE
state = c === '"' ? 4 : null; // FIXME Handle rubbish better
break;
case 4: // SD-VALUE
if(c === '\\' && !ignore) {
ignore = true;
} else if(c === '"' && !ignore) {
sdStructure[sdId][sdParam] = sdValue;
sdParam = '', sdValue = '';
state = 2;
} else {
sdValue += c;
ignore = false;
}
break;
default:
break;
}
}
return sdStructure;
};
/*
* Make sense of signed block messages
* @param {Object} block the parsed structured data containing signed data
* @return {Object} validatedBlock translated and named values, binary
* elements will be Buffer objects, if available
*/
GlossyParser.prototype.parseSignedBlock = function(block) {
if(typeof block != 'object') return false;
var signedBlock = { };
var validatedBlock = { };
// Figure out where in the object the keys live...
if(block.structuredData && block.structuredData.ssign) {
signedBlock = block.structuredData.ssign;
} else if(block.ssign) {
signedBlock = block.ssign;
} else if(block.VER) {
signedBlock = block;
} else {
return false;
}
var versionMatch = signedBlock.VER.match(/^(\d{2})(\d|\w)(\d)$/);
if(versionMatch !== null) {
validatedBlock.version = versionMatch[1];
validatedBlock.hashAlgorithm = parseInt(versionMatch[2], 10);
validatedBlock.hashAlgoString = signedBlockValues.hashAlgorithm[validatedBlock.hashAlgorithm];
validatedBlock.sigScheme = parseInt(versionMatch[3], 10);
}
validatedBlock.rebootSessionID = parseInt(signedBlock.RSID, 10);
validatedBlock.signatureGroup = parseInt(signedBlock.SG, 10);
validatedBlock.signaturePriority = parseInt(signedBlock.SPRI, 10);
validatedBlock.globalBlockCount = parseInt(signedBlock.GBC, 10);
validatedBlock.firstMsgNumber = parseInt(signedBlock.FMN, 10);
validatedBlock.msgCount = parseInt(signedBlock.CNT, 10);
validatedBlock.hashBlock = signedBlock.HB.split(/\s/);
// Check to see if we're in node or have a Buffer type
if(typeof Buffer == 'function') {
for(var hash in validatedBlock.hashBlock) {
validatedBlock.hashBlock[hash] = new Buffer(
validatedBlock.hashBlock[hash], encoding='base64');
}
validatedBlock.thisSignature = new Buffer(
signedBlock.SIGN, encoding='base64');
} else {
validatedBlock.thisSignature = signedBlock.SIGN;
}
return validatedBlock;
};
/*
* Make sense of signed certificate messages
* @param {Object} block the parsed structured data containing signed data
* @return {Object} validatedBlock translated and named values, binary
* elements will be Buffer objects, if available
*/
GlossyParser.prototype.parseSignedCertificate = function(block) {
if(typeof block != 'object') return false;
var signedBlock = { };
var validatedBlock = { };
// Figure out where in the object the keys live...
if(block.structuredData && block.structuredData['ssign-cert']) {
signedBlock = block.structuredData['ssign-cert'];
} else if(block['ssign-cert']) {
signedBlock = block['ssign-cert'];
} else if(block.VER) {
signedBlock = block;
} else {
return false;
}
var versionMatch = signedBlock.VER.match(/^(\d{2})(\d|\w)(\d)$/);
if(versionMatch !== null) {
validatedBlock.version = versionMatch[1];
validatedBlock.hashAlgorithm = parseInt(versionMatch[2], 10);
validatedBlock.hashAlgoString = signedBlockValues.hashAlgorithm[validatedBlock.hashAlgorithm];
validatedBlock.sigScheme = parseInt(versionMatch[3], 10);
}
validatedBlock.rebootSessionID = parseInt(signedBlock.RSID, 10);
validatedBlock.signatureGroup = parseInt(signedBlock.SG, 10);
validatedBlock.signaturePriority = parseInt(signedBlock.SPRI, 10);
validatedBlock.totalPayloadLength = parseInt(signedBlock.TPBL, 10);
validatedBlock.payloadIndex = parseInt(signedBlock.INDEX, 10);
validatedBlock.fragmentLength = parseInt(signedBlock.FLEN, 10);
var payloadFragment = signedBlock.FRAG.split(/\s/);
validatedBlock.payloadTimestamp = this.parseTimeStamp(payloadFragment[0]);
validatedBlock.payloadType = payloadFragment[1];
validatedBlock.payloadName = signedBlockValues.keyBlobType[payloadFragment[1]];
if(typeof Buffer == 'function') {
validatedBlock.keyBlob = new Buffer(
payloadFragment[2], encoding='base64');
validatedBlock.thisSignature = new Buffer(
signedBlock.SIGN, encoding='base64');
} else {
validatedBlock.keyBlob = payloadFragment[2];
validatedBlock.thisSignature = signedBlock.SIGN;
}
return validatedBlock;
};
if(typeof module == 'object') {
module.exports = new GlossyParser();
}