Skip to content

Instantly share code, notes, and snippets.

@warriordog
Last active June 15, 2022 16:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save warriordog/840e4a3e98c01987a32221b13233383a to your computer and use it in GitHub Desktop.
Save warriordog/840e4a3e98c01987a32221b13233383a to your computer and use it in GitHub Desktop.
Scan log files for suspicious strings
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* IMPORTANT! This version is outdated!
* This script has been completely rewritten and upgraded, and is now available here: https://github.com/warriordog/little-log-scan
* You should strongly consider using the upgraded version instead of this prototype script.
*/
/*
* Scans log files for suspicious strings.
* Intended for webserver logs, but usable with any text-based log file.
*
* Please note that this is NOT security software!
* This script is a simple tool intended for research and forensic purposes.
* There is no gaurantee of accuracy or completeness.
* False positives and false negatives are expected.
* The authors are not responsible for any negative impact due to use of this software.
*
* Important note regarding use of regular expressions:
* This script is highly regex-based, which introduces certain weaknesses.
* Effort has been made to detect and handle anomolous input, but there is a limit to the capabilities of V8's RegEx engine.
* The main things to be aware of are:
* * No protection against regex DOS. A carefully-crafted input can consume extreme amounts of CPU and memory resources.
* * Minimal protection against log injection. As this script is not (currently) aware of log formats,
* there is no good way to detect this attack. What protection *does* exist is format-checking and treating all
* input as untrusted.
*
* Author: Hazel Koehler
* Version: 1.4.0
* Modified: 2022-06-12
*
* Changes:
* 1.4.0 2022.06-12 Add --version command
* Reorder cleaners based on expected order of appearance
* Cleanup: Normalize rule names
* Remove pluralization
* Add versions wherever possible
* Split Payload/Executable into Generic, Windows, and Linux variants
* Cleanup: Improve command argument parsing
* Cleanup: Add necessary documentation for public release
* Cleanup: Add code license
* Cleanup: Remove dead and commented code
* 1.3.0 2022-06-11 Add option to disable writing full match
* Rework output options to be true/false flags
* Decode HTTP Authorization header
* Decode calls to md5sum
* Add Vulnerability/TVT DVR RCE
* Add Payload/Downloader/generic
* Add Payload/Scripts/PHP
* Add Payload/Scripts/Shebang
* Fix missing sanitization
* 1.2.1 2022-06-10 Fix overzealous GeoVision rules
* Fix Log4J base64 decoding
* Decode Log4J /Command/Base64/ format
* Decode Log4J encoded symbols (ex ${lower::}, ${env:BARFOO:-:})
* 1.2.0 2022-06-10 Include/exclude rules
* More specific vuln / malware signatures
* Improvements to Exploit/Traversal, Malware/Webshell/Generic, and Payload/Executables
* Fixes to Log4J obfuscation decoding
* 1.1.1 2022-06-09 Detect wget, curl, and netcat
* 1.1.0 2022-06-08 Add CLI arguments
* Implement tab-delimited output
* Implement verbose output
* 1.0.x 2021-12-xx Early development, no changelog
*/
import * as readline from 'node:readline';
/**
* @typedef {object} Rule
* @property {string} name
* @property {(cleaned: string, raw: string) => (IterableIterator<RegExpMatchArray>)} match
* @property {(match: RegExpMatchArray, cleanedLine: string, rawLine: string) => string} [decode]
*/
/**
* @typedef {(line: string) => string} Cleaner
*/
/**
* @typedef {(string: string) => string} Sanitizer
*/
const escapes = {
'\x00': '\\0',
'\x09': '\\t',
'\x0A': '\\n',
'\x0D': '\\r',
};
/** @type {Sanitizer[]} */
const sanitizers = [
// Escape special characters
// From https://stackoverflow.com/a/24231346
string => string.replace(/[^ -~]/g, match => escapes[match] || `\\u{${match.charCodeAt(0).toString(16).toUpperCase(0)}}`)
];
/** @type {Rule[]} */
const allRules = [
{
// Expanded list from https://stackoverflow.com/a/4669755
// 2022-06-10: URLs can contain []
name: 'Exploit/Traversal',
match: cleaned => cleaned.matchAll(/[A-Za-z0-9\-._~!$&'()*+,;=:@\/?\[\]]*\/\.\.\/[A-Za-z0-9\-._~!$&'()*+,;=:@\/?\[\]]*/g)
},
{
name: 'Payload/Script/PHP',
match: cleaned => cleaned.matchAll(/<(?:\??php\b|\?=)/gi)
},
{
name: 'Payload/Script/Shebang',
match: cleaned => cleaned.matchAll(/#!\/?(?:[\w\.\-]+\/)*\w+\s/gi)
},
{
name: 'Payload/Shell/Linux',
match: cleaned => cleaned.matchAll(/\b(?:chmod|bash|sudo|echo|cat)(?=[\s<>|&])/gi)
},
{
name: 'Payload/Shell/Windows',
match: cleaned => cleaned.matchAll(/\b(?:powershell|cmd|echo|jscript|cscript)(?=[\s<>|&])/gi)
},
{
name: 'Payload/Eval',
match: cleaned => cleaned.matchAll(/\b(?:exec|eval)(?=[\s(])/gi)
},
{
name: 'Payload/Executable/Generic',
match: cleaned => cleaned.matchAll(/\.(?:py|class|jar|war|ear|rb)\b/gi)
},
{
name: 'Payload/Executable/Windows',
match: cleaned => cleaned.matchAll(/\.(?:bat|cmd|ps[12]|vbs|vba|lnk)\b/gi)
},
{
name: 'Payload/Executable/Linux',
match: cleaned => cleaned.matchAll(/\.(?:sh)\b/gi)
},
{
name: 'Payload/Downloader/generic',
match: cleaned => cleaned.matchAll(/\b(?:wget|curl|nc|Net\.WebClient|Invoke-WebRequest|bitsadmin)\b/gi)
},
{
name: 'Payload/Downloader/netcat',
match: cleaned => cleaned.matchAll(/\bnc(?:\s*-\w\s?[\w:-]*)*\s+\w+(?:\.\w+)*\s+\d+-?\d*/gi)
},
{
name: 'Payload/Downloader/wget',
match: cleaned => cleaned.matchAll(/\bwget(?:\s+-\w\s?[\w:\/\\\.\-_%]*|\s+--[\w_\-]+=[\w:\/\\\.\-_%]+)*\s+[\w:\/\\\.\-_%]+/gi)
},
{
name: 'Payload/Downloader/curl',
match: cleaned => cleaned.matchAll(/\bcurl(?:\s+-{1,2}[\w\-]+\s?[\w:\/\\\.\-_%]*)*\s+[\w:\/\\\.\-_%]+/gi)
},
{
name: 'Obfuscation/Interpolation',
match: cleaned => cleaned.matchAll(/\$+\{[\w:-]*\w+[\w:-]*\}/g)
},
{
// See https://packetstormsecurity.com/files/130807/Ckeditor-4.4.7-Shell-Upload-Cross-Site-Scripting.html
name: 'Vulnerability/CKEditor/v4.4.7/RCE',
match: cleaned => cleaned.matchAll(/\bf?ckeditor[\/\\](?:editor|_?samples)[\/\\(?:plugins|php|filemanager)]/gi)
},
{
// See https://github.com/tothi/pwn-hisilicon-dvr/blob/master/README.adoc
name: 'Vulnerability/HiSilicon DVR/RCE',
match: cleaned => cleaned.matchAll(/\/mnt\/mtd\/Config\/Account1/gi)
},
{
// See https://gist.github.com/code-machina/bae5555a771062f2a8225fd4731ae3f7
name: 'Vulnerability/FortiOS/CVE-2018-13379 Path Traversal',
match: cleaned => cleaned.matchAll(/\/remote\/fgt_lang\?/gi)
},
{
// See https://blog.karatos.in/a?ID=01350-dad9c8fa-17e5-403c-9d7c-d26d82e59ec9
name: 'Vulnerability/PHPCMS/Auth Bypass',
match: cleaned => cleaned.matchAll(/\/api\.php\?[\w%_\.\-=&\\\/]*cachefile=[\w%_\.\-=&\\\/]*/gi)
},
{
// See https://www.exploit-db.com/exploits/39342
name: 'Vulnerability/Wordpress/plugin/appointment-booking-calendar/v1.1.24/SQLi',
match: cleaned => cleaned.matchAll(/\/wp-admin\/admin-ajax\.php\?[\w%_\.\-=&\\\/]*action=cpabc_appointments_calendar_update[\w%_\.\-=&\\\/]*/gi)
},
{
// See https://packetstormsecurity.com/files/136627/WordPress-Multiple-Meta-Box-1.0-SQL-Injection.html
// See https://gist.github.com/nikcub/9a4d68827e3770587287c254cbf7361a
name: 'Vulnerability/Wordpress/plugin/multi-meta-box/v1.0/SQLi',
match: cleaned => cleaned.matchAll(/\/wp-admin\/admin\.php\?[\w%_\.\-=&\\\/]*page=multi_metabox_listing[\w%_\.\-=&\\\/]*/gi)
},
{
// See https://www.exploit-db.com/exploits/36559
name: 'Vulnerability/Wordpress/plugin/aspose-doc-exporter/v1.0/Arbitrary File Download',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/aspose-doc-exporter\/aspose_doc_exporter_download\.php/gi)
// To capture more, add \?[\w%_\.\-=&\\\/]*file=[\w%_\.\-=&\\\/]*
},
{
// See https://www.exploit-db.com/exploits/35378
// See https://nvd.nist.gov/vuln/detail/CVE-2014-9119
name: 'Vulneravility/Wordpress/plugin/db-backup/v4.5/CVE-2014-9119 Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/db-backup\/download\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/23970
name: 'Vulnerability/Wordpress/plugin/google-document-embedder/v2.4.6/Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/google-document-embedder\/libs\/pdf\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/35371
name: 'Vulnerability/Wordpress/plugin/google-document-embedder/v2.5.14/SQLi',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/google-document-embedder\/view\.php/gi)
},
{
// See https://wpscan.com/vulnerability/90034817-dee7-40c9-80a2-1f1cd1d033ee
name: 'Vulnerability/Wordpress/plugin/cherry-plugin/v1.2.6/Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/cherry-plugin\/admin\/import-export\/download-content\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/35460
name: 'Vulnerability/Wordpress/plugin/google-mp3-audio-player/v1.0.11/Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/google-mp3-audio-player\/direct_download\.php/gi)
},
{
// See https://www.tenable.com/plugins/nessus/62205
name: 'Vulnerability/Wordpress/plugin/mac-dock-gallery/v2.9/Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/mac-dock-gallery\/macdownload\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/19056
name: 'Vulnerability/Wordpress/plugin/mac-dock-gallery/v2.7/Arbitrary File Upload',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/mac-dock-gallery\/upload-file\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/17868
name: 'Vulnerability/Wordpress/plugin/mini-mail-dashboard-widget/v1.36/Remote File Inclusion',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/mini-mail-dashboard-widgetwp-mini-mail\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/3814
name: 'Vulnerability/Wordpress/plugin/mygallery/v1.4b4/CVE-2007-2426 Remote File Inclusion',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/mygallery\/myfunctions\/mygallerybrowser\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/37752
name: 'Vulnerability/Wordpress/plugin/recent-backups/v0.7/Arbitrary File Disclosure',
match: cleaned => cleaned.matchAll(/\/wp-content\/plugins\/recent-backups\/download-file\.php/gi)
},
{
// See https://www.exploit-db.com/exploits/25305
name: 'Vulnerability/ColdFusion/v10/Credential Disclosure',
match: cleaned => cleaned.matchAll(/\/CFIDE\/adminapi\/customtags\/l10n\.cfm/gi)
},
{
// See https://github.com/alt3kx/CVE-2022-1388_PoC
name: 'Vulnerability/F5 BIG-IP/CVE-2022-1388 RCE',
match: cleaned => cleaned.matchAll(/\/mgmt\/tm\/util\/bash/gi)
},
{
// See https://securitynews.sonicwall.com/xmlpost/hackers-actively-targeting-remote-code-execution-vulnerability-on-zyxel-devices/
name: 'Vulnerability/ZyXEL/CVE-2020-9054 RCE',
match: cleaned => cleaned.matchAll(/\/adv,\/cgi-bin\/weblogin\.cgi/gi)
},
{
// See https://vuldb.com/?id.165451
name: 'Vulnerability/ZeroShell/v3.9.3/CVE-2020-29390 Command Injection',
match: cleaned => cleaned.matchAll(/\/cgi-bin\/kerbynet\?[\w%_\.\-=&\\\/]*StartSessionSubmit/gi)
},
{
// See https://www.exploit-db.com/exploits/49096
name: 'Vulnerability/ZeroShell/v3.9.0/CVE-2019-12725 Command Injection',
match: cleaned => cleaned.matchAll(/\/cgi-bin\/kerbynet\?[\w%_\.\-=&\\\/]*Action=x509List/gi)
},
{
// See https://www.exploit-db.com/exploits/43984
name: 'Vulnerability/Axis Camera RCE',
match: cleaned => cleaned.matchAll(/\/incl\/image_test\.shtml/gi)
},
{
// See https://www.exploit-db.com/exploits/43982
name: 'Vulnerability/GeoVision Camera/Auth Bypass',
match: cleaned => cleaned.matchAll(/\/UserCreat\.cgi/gi)
},
{
// See https://www.exploit-db.com/exploits/43982
name: 'Vulnerability/GeoVision Camera/RCE',
match: cleaned => cleaned.matchAll(/\/(?:PictureCatch|JpegStream)\.cgi/gi)
},
{
// See https://www.exploit-db.com/exploits/43982
name: 'Vulnerability/GeoVision Camera/Unauthorized Access',
match: cleaned => cleaned.matchAll(/\/(?:geo-cgi\/sdk_config_set|geo-cgi\/sdk_fw_check|PSIA\/System\/)\.cgi/gi)
},
{
// See https://www.exploit-db.com/exploits/43982
name: 'Vulnerability/GeoVision Camera/Double Free',
match: cleaned => cleaned.matchAll(/\/PSIA\/System\/configurationData/gi)
},
{
// See https://www.exploit-db.com/exploits/43982
name: 'Vulnerability/GeoVision Camera/Stack Overflow',
match: cleaned => cleaned.matchAll(/\/(?:geo-cgi\/param|Login3gpp)\.cgi/gi)
},
{
// See https://blog.katastros.com/a?ID=01800-20b4ee97-1675-4d3a-9baf-d2c6a0af4564
name: 'Vulnerability/ThinkCMF/RCE',
match: cleaned => cleaned.matchAll(/\/\?[\w%_\.\-=&\\\/]*(?:(?:a=fetch|templateFile=)[\w%_\.\-=&\\\/]*){2}/gi)
},
{
// See https://github.com/mcw0/PoC/blob/master/TVT_and_OEM_IPC_NVR_DVR_RCE_Backdoor_and_Information_Disclosure.txt
name: 'Vulnerability/TVT DVR/RCE',
match: cleaned => cleaned.matchAll(/\/editBlackAndWhiteList/gi)
},
{
name: 'Vulnerability/Log4j/v2.16.0/CVE-2021-44228 RCE (Log4Shell)',
match: cleaned => cleaned.matchAll(/\$+\{jndi:[^\}#]+\}/gi), // # is excluded because that is an indicator of CVE-2021-45046
decode: ([match]) => match.replaceAll(/Base64\/([A-Za-z0-9+\/=]+)/g, (pattern, base64) => {
try {
return Buffer.from(base64, 'base64').toString('utf-8');
} catch {
return pattern;
}
})
},
{
// See https://www.lunasec.io/docs/blog/log4j-zero-day-severity-of-cve-2021-45046-increased/
name: 'Vulnerability/Log4j/v2.17.0/CVE-2021-45046 RCE (Log4Shell 2)',
match: cleaned => cleaned.matchAll(/\$+\{(?:ctx:[^\}]+|jndi:([^\}]+#[^\}]+))\}/gi),
decode: ([match, rcePayload]) => {
// CVE-2021-45046 allows RCE by inserting a #.
// That is skipped by the above rule so we need to decode it here.
if (rcePayload) {
return match.replaceAll(/Base64\/([A-Za-z0-9+\/=]+)/g, (pattern, base64) => {
try {
return Buffer.from(base64, 'base64').toString('utf-8');
} catch {
return pattern;
}
});
}
// Don't decode anything else (DoS / info leak payloads)
return match
}
},
{
// Seehttps://www.exploit-db.com/exploits/50702
name: 'Vulnerability/PHPUnit/v4.8.28/CVE-2017-9841 RCE',
match: cleaned => cleaned.matchAll(/phpunit(\/\w+)*\/eval-stdin.php/gi)
},
{
// See https://en.wikipedia.org/wiki/Shellshock_(software_bug)
// See https://nvd.nist.gov/vuln/detail/cve-2014-6271
name: 'Vulnerability/GNU Bash/v4.3/CVE-2014-6271 RCE (Shellshock)',
match: cleaned => cleaned.matchAll(/(\s*)\s*\{\s*:\s*;\s*\}\s*;/g)
},
{
// See https://www.cve.org/CVERecord?id=CVE-2014-7169
name: 'Vulnerability/GNU Bash/v4.3 bash43-025/CVE-2014-7169 RCE (Shellshock 2)',
match: cleaned => cleaned.matchAll(/(\s*)\s*\{\s*(\w+)\s*=\s*>\s*\\/g)
},
{
// See https://blog.lumen.com/new-mozi-malware-family-quietly-amasses-iot-bots/
name: 'Malware/Mozi',
match: cleaned => cleaned.matchAll(/Mozi\.m/g)
},
{
name: 'Malware/Webshell/Generic',
match: cleaned => cleaned.matchAll(/\/(?:\?p4yl04d=|shell\?)[\w%\-\.&=\\\/]+/gi)
},
{
name: 'Malware/Webshell/Sp3ctra',
match: cleaned => cleaned.matchAll(/sp3ctra_XO\.php/gi)
}
];
/** @type {Cleaner[]} */
const cleaners = [
// Align slashes
line => line.replaceAll(/\\/g, '/'),
// Decode percent encoding
line => line.replaceAll(/%([0-9A-Fa-f]{2})/g, ((match, hex) => {
const asciiValue = parseInt(hex, 16);
return String.fromCharCode(asciiValue);
})),
// Decode HTTP Basic auth header
line => line.replaceAll(/\b(Basic\s+)([A-Za-z0-9+\/=]+)/gi), (match, prefix, b64) => {
try {
// Make sure to put the prefix back
return prefix + Buffer.from(b64, 'base64').toString('utf-8');
} catch {
return match;
}
},
// Decode Log4j expressions
// 2022-06-10: Some of these allow symbols in the latter part
line => line.replaceAll(/\$+\{\s*[\w:]*:-([^$}]+)\s*\}/g, (_, char) => char), // ${foo:-a}, ${::-a}, and ${foo:bar:-a} formats
line => line.replaceAll(/\$+\{\s*(upper|lower):([^$}]+)\s*\}/gi, (_, operator, char) => { // ${lower:a} and ${upper:a} formats
const op = operator.toLowerCase();
if (op === 'lower') return char.toLowerCase();
if (op === 'upper') return char.toUpperCase();
return char;
}),
line => line.replaceAll(/\$+(upper|lower)\s*\{([^$}]+)\}\s*\}/gi, (_, operator, char) => { // $$lower{a} and $$upper{a} formats
const op = operator.toLowerCase();
if (op === 'lower') return char.toLowerCase();
if (op === 'upper') return char.toUpperCase();
return char;
}),
line => line.replaceAll(/\$+\{\s*base64:([A-Za-z0-9+\/=]+)\s*\}/gi, (match, b64) => { // ${base64:==} format
try {
return Buffer.from(b64, 'base64').toString('utf-8');
} catch {
return match;
}
}),
line => line.replaceAll(/\/base64\/([A-Za-z0-9+\/=]+)/gi, (match, b64) => { // /Base64/abc format
try {
// Make sure to put the / back
return '/' + Buffer.from(b64, 'base64').toString('utf-8');
} catch {
return match;
}
}),
// Decode TVT ${IFS} inserts
line => {
// Try to limit to this one device
if (/\/editBlackAndWhiteList/i.test(line)) {
// Replace ${IFS} with a space
return line.replaceAll(/\$\{\s*IFS\s*\}/gi, ' ');
} else {
return line;
}
},
// Decode calls to md5sum. Current only handles echo piped to md5sum.
line => line.replaceAll(/\b(echo(?:\s+[\-\w]+)*\s)([A-Za-z0-9+\/=]+)\s*\|\s*md5sum(?:\s+[\-\w]+)*/gi, (match, prefix, b64) => {
try {
return prefix + Buffer.from(b64, 'base64').toString('utf-8');
} catch {
return match;
}
})
];
/** @typedef {{key: string, keyRaw: string, value: string, raw: string, duplicate: CliArg | null}} CliArg */
/** @type {Record<string, CliArg>} */
const args = process.argv
.slice(2)
.map(arg => arg.match(/([^=]+)(?:=(.*))?/))
.reduce((map, [argString, argKeyRaw, argValue]) => {
const argKey = argKeyRaw.toLowerCase();
map[argKey] = {
key: argKey,
keyRaw: argKeyRaw,
value: argValue,
raw: argString
};
return map;
}, {});
function getFlagArg(arg) {
return !!args[arg];
}
function getValueArg(arg, lower = false, def = null) {
const argObj = args[arg];
if (!argObj) {
return def;
}
if (lower) {
return argObj.value.toLowerCase()
} else {
return argObj.value;
}
}
function getRepeatedArg(arg, lower = false, def = []) {
const argObj = args[arg];
if (!argObj) {
return def;
}
const results = [];
for (let curArg = argObj; curArg != null; curArg = curArg.duplicate) {
let value = curArg.value;
if (lower) {
value = value.toLowerCase();
}
results.push(value);
}
return results;
}
if (getFlagArg('--help')) {
console.log('Scans webserver log lines to detect suspicious behavior.')
console.log('Logs should be piped through standard input, one line per log.');
console.log('Findings will be printed to stdout, one line per finding.');
console.log('All output is automatically sanitized to remove non-ascii-printable characters.');
console.log();
console.log('Depending on flags, output may contain the following components:');
console.log('Finding: Name of the rule that matched the output.');
console.log('Index: Index (line number) of the line that was matched. This is counted by lines received as piped input, not lines in the original file.');
console.log('Match: Portion of the line that was detected. This is automatically decoded / deobfuscated, if possible.');
console.log('Line: Entire content of the matched line. This is automatically decoded / deobfuscated, if possible.');
console.log('Raw: Same as "line", but NOT decoded. Only the standard sanitization is applied.');
console.log();
console.log('Standard output formats:');
console.log('|TSV|Raw|Clean|Format |Notes |');
console.log('| X | X | X | [Finding]\\t[Index]\\t[Match]\\t[Line]\\t[Raw] | |');
console.log('| X | | X | [Finding]\\t[Index]\\t[Match]\\t[Line] | |');
console.log('| | X | X | [Finding]: [Index] {[Match]} {[Line]} {[Raw]} |Index is left-padded to 9 characters|');
console.log('| | | X | [Finding]: [Index] {[Match]} {[Line]} |Index is left-padded to 9 characters|');
console.log('| X | X | | [Finding]\\t[Index]\\t[Match]\\t[Raw] | |');
console.log('| X | | | [Finding]\\t[Index]\\t[Match] | |');
console.log('| | X | | [Finding]: [Index] {[Match]} {[Raw]} |Index is left-padded to 9 characters|');
console.log('| | | | [Finding]: [Index] {[Match]} |Index is left-padded to 9 characters|');
console.log();
console.log('Usage: find-suspicious-logs [options]');
console.log('--help Print help and exit.');
console.log('--version Print version and exit.');
console.log('--tsv Output in TSV (tab-delimited) format.');
console.log('--cleaned=<Y/N> Include the entire cleaned, decoded line in the output. Defaults to Y (on).')
console.log('--raw=<Y/N> Include the entire raw, undecoded line in the output. Defaults to N (off)');
console.log('--include=<patterns> Patterns to include rules (comma separated). Only matching rules will be run.');
console.log('--exclude=<patterns> Patterns to exclude rules (comma separated). Overrides --include option..');
console.log('--list-rules List all rules and exit.')
// Stop executing after processing help.
process.exit(0);
}
if (getFlagArg('--version')) {
console.log('find-suspicious-logs version 1.4.0 (2022-06-12)');
process.exit(0);
}
if (getFlagArg('--list-rules')) {
// Sort rules alphabetically
const sortedRules = Array.from(allRules)
.sort((a, b) => a.name.localeCompare(b.name));
// Print all rules
for (const rule of sortedRules) {
console.log(rule.name);
}
// Stop executing after printing rules.
process.exit(0);
}
// Parse flags
const isTsv = getFlagArg('--tsv');
const includeVerbose = getValueArg('--raw', true, 'n') === 'y';
const includeCleaned = getValueArg('--cleaned', true, 'y') === 'y';
// Select rules
const includes = getRepeatedArg('--include', false, []).flatMap(include => include.split(','));
const excludes = getRepeatedArg('--exclude', false, []).flatMap(exclude => exclude.split(','));
const rules = allRules.filter(rule => {
// If includes are specified, AND none of them match, then exclude this rule.
if (includes.length > 0 && !includes.some(include => rule.name.includes(include))) {
return false;
}
// If any exclude matches, then exclude this rule
if (excludes.some(exclude => rule.name.includes(exclude))) {
return false;
}
return true;
});
/**
* @param {TemplateStringsArray} strings
* @param {...unknown} values
* @returns {string}
*/
function sanitize(strings, ...values) {
const parts = [ strings[0] ];
for (let i = 0; i < values.length; i++) {
const value = sanitizers.reduce((val, san) => san(val), String(values[i]));
parts.push(value);
parts.push(strings[i + 1]);
}
return parts.join('');
}
/**
* @param {Rule} rule
* @param {string} match
* @param {string} cleaned
* @param {string} raw
* @param {number} lineNumber
*/
function writeMatch(rule, match, cleaned, raw, lineNumber) {
const log = getMatchString(rule, match, cleaned, raw, lineNumber);
console.log(log);
}
/**
* @param {Rule} rule
* @param {string} match
* @param {string} cleaned
* @param {string} raw
* @param {number} lineNumber
* @returns {string}
*/
function getMatchString(rule, match, cleaned, raw, lineNumber) {
if (isTsv) {
if (includeCleaned) {
if (includeVerbose) {
return sanitize`${rule.name}\t${lineNumber}\t${match}\t${cleaned}\t${raw}`;
} else {
return sanitize`${rule.name}\t${lineNumber}\t${match}\t${cleaned}`;
}
} else {
if (includeVerbose) {
return sanitize`${rule.name}\t${lineNumber}\t${match}\t${raw}`;
} else {
return sanitize`${rule.name}\t${lineNumber}\t${match}`;
}
}
} else {
if (includeCleaned) {
if (includeVerbose) {
return sanitize`${rule.name}: ${String(lineNumber).padStart(9, ' ')} {${match}} {${cleaned}} {${raw}}`;
} else {
return sanitize`${rule.name}: ${String(lineNumber).padStart(9, ' ')} {${match}} {${cleaned}}`;
}
} else {
if (includeVerbose) {
return sanitize`${rule.name}: ${String(lineNumber).padStart(9, ' ')} {${match}} {${raw}}`;
} else {
return sanitize`${rule.name}: ${String(lineNumber).padStart(9, ' ')} {${match}}`;
}
}
}
}
/**
* @param {string} line
* @returns {string}
*/
function cleanLine(line) {
while (true) {
const newLine = cleaners.reduce((l, cleaner) => cleaner(l), line);
if (newLine === line) {
break;
}
line = newLine;
}
return line;
}
/**
* @param {string} raw
* @param {number} lineNumber
*/
function processLine(raw, lineNumber) {
// Run all cleaners
const cleaned = cleanLine(raw);
// Run all rules
for (const rule of rules) {
// Apply the rule
const matches = rule.match(cleaned, raw);
// Decode and print all matches
for (const match of matches) {
// Decode the line, or fall back to the raw match
const decoded = rule.decode ? rule.decode(match, cleaned, raw) : match[0];
// Write output
writeMatch(rule, decoded, cleaned, raw, lineNumber);
}
}
}
// Count the lines
let lineNumber = 0;
// Read STDIN until close
const rl = readline.createInterface({ input: process.stdin });
rl.on('line', line => {
// Process each incoming line
processLine(line, lineNumber);
lineNumber++;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment