From bd72fdae36892f8baeee44b4a4679a8a330e5962 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Thu, 24 Aug 2017 07:01:40 -0400 Subject: [PATCH] Upstream source. --- Bugs | 2 + Changes | 786 ++++++++ LICENSE | 17 + Makefile | 65 + README | 90 + amavis-logwatch | 4067 ++++++++++++++++++++++++++++++++++++++++ amavis-logwatch.1 | 923 +++++++++ amavis-logwatch.1.html | 888 +++++++++ amavis-logwatch.conf | 224 +++ 9 files changed, 7062 insertions(+) create mode 100644 Bugs create mode 100644 Changes create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 amavis-logwatch create mode 100644 amavis-logwatch.1 create mode 100644 amavis-logwatch.1.html create mode 100644 amavis-logwatch.conf diff --git a/Bugs b/Bugs new file mode 100644 index 0000000..15afe64 --- /dev/null +++ b/Bugs @@ -0,0 +1,2 @@ +- max_report_width doesn't fully limit width on all reports; currently + it controls the Detailed section output. diff --git a/Changes b/Changes new file mode 100644 index 0000000..d6c9401 --- /dev/null +++ b/Changes @@ -0,0 +1,786 @@ +2014-05-01 (version: 1.51.03) + - Fix: Update RE for TIMING lines to accomodate new output from amavis 2.8.1 with + Unix::Getrusage installed. + - Fix: Ignore 2.8.1's RUSAGE report + - Fix: increment spamdiscarded category on do_notify_and_quarantine: cutoff, blacklisted. + - Fix: Some log lines that were no longer being ignored (minor changes) + - Fix: Ignore some DSPAM lines: Thanks: Christian Rößner + - Change: Several minor changes in the SADiags section: "SA info: dns: [...]" lines + are ignored; canonicalize some PIDs and IDs, replacing them with with generic + "" and "" strings to allow grouping. + - Fix: Handle new amavisd startup messages + - Fix: Handle format changes for Amavisd-new 2.9.0 + +2012-02-27 (version: 1.51.02) + - Fix: Ignore case of SPAM-TAG lines. Amavisd-new 2.7.0 changed the + SPAM-TAG line to Spam-tag, and changed its log level to 3. This + log entry is used to trigger spam tagged counts. Users on 2.7.0 + will have to increase log_level to 3, or change the amavisd-new + source code as per the patch located in this message: + http://lists.amavis.org/pipermail/amavis-users/2012-January/001163.html + - Fix: Ignore more debug/misc log lines + - Thanks: Stefan Jakobs + +2012-01-14 (version: 1.51.01) + - Fix: Removed old references to $re_DSN, $re_QID, $re_DDD (obsoleted + and removed from shared packages) which prevented amavis-logwatch + from runnning. + +2012-01-11 (version: 1.51.00) + - Change: License is now the MIT/X-Consortium License: + http://www.opensource.org/licenses/mit-license.php + This allows re-inclusion into the logwatch project. + +2011-07-08 (version: 1.50.04) + - Fix: Support 2.7's log_short_templ changes ([:client_addr_port], + {actions_performed}). + - Fix: Support 2.7's TIMING lines, which now start with "size: " + - Fix: Support 2.7's new BAD-HEADER-[:ccat|minor]; The stuff after + BAD-HEADER is ignored. + - Fix: Ignore log parser compatibility "header_edits_for_quar..." lines. + - Fix: Additional minor 2.7 log line changes + - Fix: Ignore more debug lines. + - Thanks: Michael Orlitzky + - Fix: change method of untaint'ing data when evaluating per-recipient + boost in Hits; required for newer perl versions. + +2010-11-12 (version: 1.50.03) + - Fix: Eliminate use of $re_IP, as it is too complicated, expensive, and did + not correctly match all valid IPv6 addresses. Thanks: Michael Orlitzky + - Fix: Ignore extra entries after tests= in SPAM lines, for the case where the + tests are not surrounded by brackets (Maia, and older amavisd versions). + - Fix: Ignore more debug lines. + Thanks: Stefan Jakobs + - Internal: Update URL to Sourceforge. + +2010-03-09 (version: 1.50.02) + - Fix: tarball was missing html documentation. Thanks: Michael Orlitzky + +2010-03-02 (version: 1.50.01) + - New: Allow enabling/djsabling the summary section in config file, using + var $postfix_Show_Summary and command line option --[no]summary. + Thanks: Benedikt Bohm + - Change: Ignore all 'wbl' log lines, until they cannot be meaningfully + summarized. + - Fix: Score frequencies buckets were not sorted correctly, causing + erroneous bucket tallies. Thanks: Victor Hugo dos Santos + - Fix: Ignored lines are placed into a list at runtime instead of + at compile time. Works around perl bug #56202 I discovered + while working on postfix-logwatch. + - Fix: Support continuation lines that include the alert markers + (!) and (!!). + - Fix: Skip RUSAGE reporting lines + - Fix: Ignore more debug lines. + - Thanks: Chris Burton, Gabriele Beltrame, Jernej Porenta, + Danny (aka "Technik"), Ricardo Stella, Armin Tueting + +2009-07-19 (version: 1.50.00) + - New: MalwareToSpam report shows whether or not amavis turned + a virus scanner infected result into a spam score, and the score + assigned. + - New: Handle some new logging messages from amavisd 2.6.3. + - New: Counter for truncated messages passed to SA (amavis >= 2.6.3). + - New: Group white/blacklisted sender addrs by domain for limiting + - New: Section SADiags for amavis reported SpamAssassin diagnostics + (log lines started with "SA warn", "SA info", etc.). + - Change: Group official vs. unofficial ClamAV detections in + MalwareByScanner (and MalwareToSpam) reports. + - Change: Level of detail now shown in Detail section's title. + - Fix: Patch to ignore "storage and lookups will..." messages + when more than one SQL server is in use. + - Fix: Don't collect SpamScores (for the spam score frequencies and + spam score percentiles reports) when not necessary. + - Fix: Better diagnostic when limiter level is not specified on + the command line using the --limit option (eg: + "--limit malwarebyscanner" v. "--limit malwarebyscanner=10"). + - Fix: Coerce previously unmatched "unknown bad table name" log + line into Warning section. + - Fix: Divide by zero error in print_summary_report(), found in + test case. + - Fix: Remove duplicates from test=[xxx,...], which occurs in + amavis < 2.6.2 when using @virus_name_to_spam_score_maps, which + would be seen in SpamAssassin Rule Hits reports as double-counted + AV:xxx hits. + - Fix: Routine to strip out trace information was too greedy. + - Fix: handle p0f lines w/short client info (eg. MYNETWORKS). + - Fix: Ignore "Error reading mail header section", which is diag + output for a condition created by a perl getline() bug. + - Fix: Handle other syslog formats. + - Fix: Update some missing options and information in man page. + - Fix: Ignore more debug lines. + - Internal: Ignored lines are now placed into a list, instead of + hard coded into the code. In the future, this will allow users + to configure ignore patterns without making code modifications. + - Thanks: Jernej Porenta, Jerzy Sakol, Chris Burton, Armin T�ting + +2008-11-13 (version: 1.49.09) + - Fix: Work around minor amavis logging error which produces + empty _WARN|_DIE lines. Thanks: Rob Sterenborg and Chris Burton + - Fix: remove debugging print which is likely never reached. + +2008-06-25 (version: 1.49.08) + - Fix: White and blacklisted was broken. Added soft-hard + distinction to reports. + - Fix: Ignore more debug log lines. + - Thanks: Brendan + - Fix: debug keyword unmatched had incorrect value assigned, + disabling the ability to output the unaltered UNMATCHED debug + lines using --debug unmatched. + - Fix: The counter "Spam tagged" was being incremented for + SPAM lines (it should only increment on SPAM-TAG lines). + Thanks: Armin T�ting + +2008-06-24 (version: 1.49.07) + - New: Added p0f (passive fingerprinting) hits by contents type, + OS, and IP address in Detail section. Controlled by level + limiter variable 'p0f', which is set to level 2 by default + (this shows OS genre, but suppresses further by-IP address + breakdown); increase to level 3 for per-IP breakdown. + This section may be moved into a suppliemental report later. + - Fix: Ignore more debug log lines. Thanks: Brendan + +2008-06-18 (version: 1.49.06) + - New: Include hit counts after each key name in Timings report. + - New: SpamAssassin timings percentiles report. Options: + sa_timings, sa_timings_percentiles. Requires amavis version + 2.6+ and SpamAssassin 3.3+. + - Change: Scan Times percentiles report units have been changed to + milliseconds rather than seconds. + - Change: Removed the "Hypothetical" row from timings reports; it + was not very meaningful. + +2008-06-11 (version: 1.49.05) + - New: Reduce some noise in smtpresponse section + - Thanks: Robert Brooks + +2008-06-10 (version: 1.49.04) + - New: Show count of Spam tagged messages in Summary. + - Fix: The fix in 1.49.02 of treating SPAMMY as ham was incorrect. + SPAMMY is spam, not ham. + - Fix: Removed extraneous spaces from end of log lines, left by + some syslog implementations. + - Fix: Ignore SQL reconnecting "NOTICE: reconnecting in response to..." + - Fix: Ignore "FuzzyOcr: Skipping ..., image too small" lines for now. + - Fix: Better handle when name field in MIME part is only whitespace + - Thanks: Robert Brooks + +2008-06-08 (version: 1.49.03) + - New: Count of penpals saved from kill reported in Summary. + - Fix: Include $amavis_Line_Style in config file. + - Fix: Clear start info earlier if "logging initialized" line is + present. Otherwise, consider "starting. " as the first line. + - Fix: Some startinfo output was occuring when --nodetail was set. + - Fix: Supplemental sections could not be re-enabled after using + --nodetail. + - Fix: SMTP: connection lines may not include size, which causes + unmatched lines. Thanks: Stefan F�rster + - Fix: In the above case, the number of bytes scanned is not available. + Byte scanned is now calculated from the size data available in the + Passed/Blocked line. Note: this number is slightly smaller than + that reported in the LMTP/SMTP connection line, so the total bytes + scanned number will be marginally reduced. + +2008-05-29 (version: 1.49.02) + - Change: level limiters are no longer unique command line options, + but are now parameters to the single option "--level" or "-l". + This reduces the number of command line options in the help list, + and simplifies the code added to support reject_reply_patterns. + For example, the config file level limiter $amavis_SpamBlocked = 2, + the command line option would be --limit spamblocked=2 or + -l SpamBlocked=2, rather than the previous --spamblocked=2. + This also means the --no variants for level limiters are gone + (eg. --nosent; instead use -l SpamBlocked=0). There is no change + within the configuration files. Limiters can still be abbreviated + so long as they are unambiguous. + - Change: dccproc is included in syslog_name in standalone mode. + - Change: nodsnsent* variants merged into single dsnsuppressed section, + which is now included in Detail section. + - Change: Moved total message scan counts/bytes to the top of the + Summary report. + - Change: Spam score percentiles report broken out by contents + category: unchecked, spam, and ham. SA is not called when contents + category is VIRUS or BANNED; UNCHECKED may have a spam score, and + anything less than SPAM is considered ham. Note: the count of + ham in the score reports may be less than the ham count shown in + the Summary section, as SA may have been bypassed (see the + SpamAssassin bypassed count in the summary report). + Encouraged by: Hannes Erven + - Change: Passed/Blocked sections are now ordered by priority, as + defined by amavis: VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH, + OVERSIZED, MTA, and CLEAN. + - New/Change: Summary section now includes both Passed/Blocked and + Ham/Spam breakdowns. Previously, only a Ham/Spam breakdown was + present, and this was insufficient to accurately show how messages + were classified. Passed/Blocked is the first grouping shown in + Summary section, and the Spam/Ham/etc. grouping is the second. + This sub-section can be suppressed with via --noby_ccat_summary or + amavis_show_by_ccat_summary = No. + - New: Option --line_style instructs how to handle lines lengths + longer than max_report_width. Options are "wrap", "full", or + "truncate" (default). The older --detail >= 11 is equivalent + to line_style=full; line_style=truncate or line_style=wrap has + higher precedence and will dictate how long lines are handled. + Option abbreviations are --truncate, --wrap, or --full. + - New: Single letter options for some long options; run with + --help to see list. More may be added in the future. + - New: Beginning of support for amavis 2.6 log entries. + Bounce killer sections: bouncekilled, bouncerescued, and + bounceunverifiable. + - New: VirusScanSkipped section. + - New: Include hit counts in spam score percentiles and spam score + frequency reports. Score is adjacent to row header. + - Fix: In spam score reports, spam scores are collected for any + contents category; previously, only ham and spam categories + were used. If a score exists, it is used. + - Fix: SPAMMY was being treated as spam instead of ham. + - Fix: Passed UNCHECKED lines were reported as Unmatched, due to + spelling error (UNCHECED v. UNCHECKED) in RE. + - Fix: At detail > 10, log lines were truncated to max_report_width. + - Fix: Ignore dkim lines until better report is created. + - Fix: Ignore more debug log lines. + Thanks: Armin T�ting + - Fix: eliminate some extraneous newline output. + - Internal: debug output is now controlled by keywords. + +2008-03-17 (version: 1.49.01pre1) + - New: Option sect_vars shows names of section configuration + variables/command line options in titles of Detail report sections. + This allows easy correlation of corresponding configuration file + variables/command line option for each section. [ Default: off ] + - New: amavis-logwatch man page created. + - New: Threshold limiting and Top N lists for every level in each + Detail section. Every level in each section can now be limited + with minimum count thresholds and top N lists. See the updated + README file, the comments in the amavis-logwatch.conf file, and + the new amavis-logwatch man page. + - New: Added Defanged and DefangError sections + - New: Autolearn report available when autolearn entries exist. This + feature requires enabling autolearn entries in amavis' $log_templ + log template, at the end of the amavis program or by proper + configuration of $log_templ in your amavisd.conf file. + Encouraged by: "Cube" + - Change: All recipients are now show when multiple recipients are + present. Previously, only the first recipient was shown. The + new option --first_recip_only will enable the previous behavior. + - Change: Timings percentiles report title and summary labels + - Change: Taint mode is now on by default in standalone mode. It is + disabled upon installation in logwatch mode, as logwatch fails with + taint mode enabled. + - Change: Summary report format has been changed (once again). + - Fix: Significantly reduce memory footprint when detail < 5. + - Fix: Usage and --help now correctly show only Detail section + level limiter options that are available. Previously, summary- + only counts were also display as level limiter options. + - Fix: configuration file reading code was not properly warning on + non-existent files + - Fix: use Received ip (%e macro in log_templ) if client IP (%a) is + not available. + - Fix: ignore diagnostic defang messages. Thanks: Leon Kolchinsky + - Fix: SpamDiscarded messages were being added to TotalSpams, but + this is incorrect, as SpamDiscarded is already included in + SpamBlocked. SpamBlocked is now reduced by SpamDiscarded, and + TotalSpams is correct again. + - Fix: Ignore more level 3+ log lines. + - Fix: unmatched SPAM-TAG lines when no quarantine enabled. + Thanks: Paul Dulaba + - Fix: sabypassed section was being double counted due to spam-scan + lines + - Fix: report header was one character too wide + - Internal: Converted source to be package-based to allow code sharing + with postfix-logwatch. The single-file executable is auto-generated + from the packages. + - Internal: internal gen_test_log function to create sample log data + for testing. Reads '#TD' comments from within amavis-logwatch. + - Internal: converted all Section keynames to lowercase to avoid + silly case errors due to hash key case differences in Sections, + Opts, and Collecting hashes. + - Internal: Cleanup Passed/Blocked code + +2008-01-25 (version: 1.48.27) + - Released + +2008-01-19 (version: 1.48.27pre5) + - Fix: Relax email address REs + - Fix: Prefix unknown data with an asterisk as in postfix-logwatch + (eg. "*unknown") + - Fix: Generalize capture of "Empty result from ..." to support + any AV scanner + Thanks: Stefan Jakobs + - Fix: Ignore more level 5 log lines. + +2008-01-16 (version: 1.48.27pre4) + - Fix: Ignore "policy protocol: policy_bank=..." lines, and + report as warnings "policy protocol: INVALID..." lines + Thanks: Jurek Sakol + - Fix: Additional WarningSQL captures. + - Fix: Ignore more level 5 log lines. + +2008-01-14 (version: 1.48.27pre3) + - Fix: Ignore ! leader chars in "FWD via SMTP: ..." and similar. + Thanks: Stefan Jakobs + +2008-01-14 (version: 1.48.27pre2) + - New: SmtpResponse section for various negative and positive + SMTP reponse log lines. + - New: TmpPreserved section to capture log lines indicating when + amavis does not remove temporary directories + - Fix: Experimental: Handle more dccproc errors in DccError + Thanks: Stefan Jakobs + - Fix: ignore "save_info_preliminary..." and "save_info_final ..." + +2007-12-14 (version: 1.48.27pre1) + - New: Handle "Quarantine release xxx: missing X-Quarantine-ID" log + entries as warnings in new section WarningNoQuarantineID. + - New: Handle "WARN: address modified ..." log entries as warnings + in new section WarningAddressModified. + +2007-12-14 (version: 1.48.26) + - Fix: Handle empty sender in release from quarantine + - Fix: Handle amavis 2.5.3's "extra modules loaded after daemonizing" + message + - New: AVTimeout is now treated as a warning so it will show at the + top of the Summary report. The scanner that timed out is available + in the Detail section. + - New: AVConnectFailure, WarningSmtpShutdown, WarningSQL sections. + These were previously grouped under the Warnings section; Like the + AVTimeout section, these too will also appear at the top of the + Summary report. + - Fix: canonicalize several warnings which differed only by process + number, amavis id, callback info, etc. Thanks: Stefan Jakobs + - Fix: several Warning Counts variables were inconsistently used. + - Fix: avoid useless reporting of Perl's pseudo-modules which can't, + be pre-loaded once (eg. those in .../unicore/lib/). + +2007-10-17 (version: 1.48.26pre4) + - Incompatible Change: some more config variable/option name changes + for naming consistency and shortening: ReleasedMsg -> Released, + CleanMsgPassed, CleanMsgBlocked -> CleanPassed, CleanBlocked respectively + - Change: uncomment all variables in the config file. This ensures the + variables in the config file stay in sync with those used in the + source. + - Fix: sync'd incorrect names in config file with source + Thanks: Isaac Ordonez + - New: Handle "Can't send SIG 0 to process [###]: Operation not permitted..." + +2007-10-14 (version: 1.48.26pre3) + - Changed: In timings report, always show actual totals instead of totals for + the data presented (eg. when --timings was < 100%). + - Changed: Total times shown in timings percentiles report previously + were the sum of the percentiles columns. This represented a cherry-picked, + hypothetical set of values. Now, there are two totals shown: the first + is a percentiles report calculated from the actual set of per-message total + scan times reported by amavis, and the second is a hypothetical worst/best + case, whose percentiles are calculated from simple column summations. + Encouraged by: Robert Brooks + +2007-10-10 (version: 1.48.26pre2) + - New: Spam score frequency report, controlled with option --score_frequency. + Suggested by: Robert Brooks + - Incompatible Change: To reduce the number of configuration file variables + and command line options, and to be more consistent, several config + variables/command line options have changed. In general, the method now + employed is an overloading approach; specifying values for a report + section also enables that section. The "no" variant disables a section, + and the new keyword "default" resets options to their internal defaults. + Run amavis-logwatch --help to see a list of the new options, and see the + comments in the default amavis-logwatch.conf file. + - Fix: IP isn't always reported; report "Unknown" in such cases + - Internal: change how options are processed + - Internal: parameterize some report formatting + +2007-10-02 (version: 1.48.26pre1) + - New: show percentiles totals at bottom of percentile report. + Suggested by: Robert Brooks + +2007-09-21 (version: 1.48.25) + - Fix: Ignore "mail checking ended: " and "AM.PDP" lines. Also, escape + a dot in an ignore pattern, and remove duplicate. + Thanks: Curtis Doty, Mark Martinec + +2007-09-17 (version: 1.48.24) + - Fix: Remove excessive blank lines in summary output + +2007-09-16 (version: 1.48.23) + - Fix: Support SMTP connections to amavis + - Experimental: Reformated summary section to better show the relationship + between counts, percentages, and totals. Feedback encouraged. + +2007-09-13 (version: 1.48.22) + - Fix: no -D option to install in FreeBSD. Use -d instead in Makefile + +2007-09-01 (version: 1.48.21) + - Fix: use ask_av and run_av as virus indicators for Malware by Scanner, + as previously used virus_scan line does not report accurate information + when multiple scanners report different malware names + - Fix: ignore "broken pipe (don't worry), retrying" messages + - Fix: Directory permission within tarball was not executable + - Fix: remove rooted path in md5 file + - Fix: Makefile install-logwatch rule was missing a parenthesis + - Fix: Makefile updates from Till Mass + +2007-08-31 (version: 1.48.20) + - Change: Include GPLv2 license + - Change: Include version number in tarball file name + - Internal: Move CVS log comments to Changes file + +2007-08-29 (version: 1.48.19) + - New: added --syslog_name ($amavis_Syslog_Name) for installations where + syslog name is not "amavis" (eg. /usr/sbin/amavisd). + New: ignore additional amavis log entries + Thanks: Jordi Espasa Clofent + +2007-08-15 (version: 1.48.18) + - Internal: change variable Formats to the more obvious named Sections + +2007-08-14 (version: 1.48.17) + - New: capture "skip local delivery..." messages, controlled with + LocalDeliverySkipped variable/option. Thanks Andrew M. Kinnard + - Output help and version info on STDOUT for easier pipelines to a pager + - Internal: split printReports into printSummaryReport and printDetailReport + +2007-08-13 (version: 1.48.16) + - New: enable detail output for clean blocked messages + - Fix: disable startinfo report when --nodetail is given + +2007-08-04 (version: 1.48.15) + - New: option --nodetail zeros out all detail levels, to more easily obtain + only specified detailed reports (eg: --nodetail --spamblocked=4) will + show only spam blocked (to level 4) in the details section. + - New: option --nosummary disables the summary section + - New: detailed section command line arguments can now be specified with the + prefix "no", to set the level to 0 (eg. --nospampassed is equivalent to + --spampassed 0). + +2007-08-03 (version: 1.48.14) + - New: option --config_file allows specifying a configuration file via + command line. Options in configuration file act as though they were + set on the command line in order, with ealier settings being over- + ridden by most recent settings. Multiple config_file options may + be specified. + - Fix: Yes/True and No/False config values weren't being read properly + in standalone + +2007-08-02 (version: 1.48.13) + - Ignore "storage and lookups will use the same connection to SQL" + - Support bracketed IP in LMTP messages (eg. "LMTP:[127.0.0.1]:10026...") + - Thanks John Beaver + +2007-07-13 (version: 1.48.12) + - FreeBSD change in v1.48.11 botched non-freeBSD systems + +2007-07-13 (version: 1.48.11) + - Support FreeBSD ( preceeds hostname in syslog) + +2007-07-06 (v1.48.10) + - Capture and summarize cabextract messages + +2007-07-05 (v1.48.9) + - Generalize archive extraction problem messages. The new variable + amavis_ArchiveExtract replaces amavis_EmptyMember and EncryptedArchive. + - Generalize some fatal and warning messages + - Capture and summarize more MAIA and MySQL entries + - Add MAIA Fake Sender section + +2007-07-03 (v1.48.8) + - Experimental: syntax to limit Top N level 1 output lines. Variables + that control depth levels in detailed reports can be specified as + m.n, where m is the maximum level to output, and n specifies the number + of level 1 items output. Eg: $amavis_SpamBlocked=2.10, will output + the top 10 level 1 items, with each item providing 2 levels of detail. + - Capture "Config files read" + +2007-07-03 (v1.48.7) + - Reorganized code; favors newer amavis releases + - Show scores assigned to each SA test in spam/ham reports + - Catch and report file(1) bad/extra output messages + - increased width of timing report for maia + - Add additional ccats MTA-BLOCKED, OVERSIZED, OTHER + - Support older amavis entries (as in maia) + +2007-06-07 + - Changed NoSubject title to more accurately indicate Subject header insertion + +2007-05-31 + - Fix bug which caused config file to be required in standalone mode + +2007-05-28 + - Added content-type section (log_level >= 2) + - Added SpamAssassin bypassed count summary ($sa_mail_body_size_limit) + - Only print report headers for SA rules report when data exists + - Fix end points of percentiles calculations + - In percentile reports, when N% is requested for output, show *at least* + N% (previously was *at most*, which might have produced no values) + + - NOTE: Some older amavis log entries may be flagged as Unmatched, as code + restructuring now favors recent amavis releases. Please report any + Unmatched entries and I will correct the problems immediately. + +2007-05-27 + - Can't ignore all do_quarantine_and_notify messages; one indicates quarantine cutoff level + - Additional continuation line fix; several log entries end with "..." and are not + continuation lines, causing subsequent log line to be ignored. Continuation lines are + exactly 980 chars - see comments in source. + - Internal: Group similar messages + - Add zero-width assertions and use strict IP RE in bycount sort subroutine + to match IP addresses more reliably + +2007-05-23 + - Fix alignment issue in Top N Spam/Ham reports + - Provide a better (working) solution to avoid the SPAM/SPAM-TAG duplication + mentioned below. + +2007-05-18 + - Allow supplementary reports to be printed even when TotalMsgs is 0; + This typically would not occur in practice, but is useful for debugging. + - Some lines ending with "..." are not actually continuation lines; treat + them as ordinary lines for further consideration (Thanks Eray Aslan) + - When both SPAM and SPAM-TAG are present, don't double count (which + doubles SA test score accumulators). (Thanks Eray Aslan) + - Rework white/blacklisted section; sender is now tracked. (Thanks Dan Horne) + +2007-05-15 + - Revision 1.48 2007/05/16 04:27:17 mrc + +2007-05-16 + - Ignore a few more log lines for log_level 3 + - Fixed problem setting SARules config variable; renamed Show_SARules + +2007-05-14 + - Revision 1.47 2007/05/14 17:28:21 mrc + - Forgot to update Version string + +2007-05-14 + - Revision 1.46 2007/05/14 17:20:45 mrc + +2007-05-08 + - Minor changes to sync up with postfix-logwatch release + - Capture MailZu quarantine release messages + +2007-05-07 + - Handle amavis' Hits that includes, but does not add/subtract boost + scores (eg. 1.03-3.5) + - The option --timing_percentiles was not being recognized, due to name + change from --percentiles. Both are recognized. + +2007-05-04 + - Ignore "The amavisd daemon is already running" messages + +2007-05-02 + - Reworked the Startup info section. Only shows at detail 10 by default + +2007-05-01 + - Add Ham / Spam hits summary + - Experimental: Show Top N Spam/Ham SA rules hit + - Minor SPAM/SPAM-TAG RE update + +2007-04-28 + - Experimental: Show hit Bayesian buckets + - Experimental: Show hit SA tests + - More cleanup (refactor common code, replace most global variables with lexicals, + lowercase non-global variable names, shorten variable names, etc.) + +2007-04-25 + - Renamed script to "amavis-logwatch" to avoid confusion when running + in standalone mode. See the README. + - Experimental: Show spam score percentiles. + - Additional standalone features. Try: logwatch_amavis/amavis --help + +2007-04-24 + - Minor RE adjustment for amavis 2.5.0 + - Report SA version when shown in INFO line (amavis 2.5.0) + - Top N percent of Timings report is now configurable (amavis_Timings) + - Display of startup info can be disabled (amavis_StartInfo) + +2007-03-31 + - Capture SMTP shutdown messages + - Capture and report TEMPFAIL messages + - Updated comments regarding IPv6 from Mark Martinec + - Support for running in standalone mode (independent of logwatch) + +2007-03-27 + - Revision 1.45 2007/03/27 01:09:48 mrc + +2007-03-25 + - Handle multiple viruses in list in Malware by Scanner + +2007-03-15 + - Handle multiple recipients in Release from quarantine + - Swap From and To keys in Release from quarantine, to handle multiple + recipients + +2007-03-03 + - add inc_unmatched subroutine for easier debug of unmatched lines + +2007-03-01 + - Fix problem in sort routine where IP addresses were being captured + anywhere in an output line for comparison via pack 'C4' - only + attempt IP comparison if an IP address is the start of an output line. + +2007-02-27 + - Revision 1.44 2007/02/27 20:31:42 mrc + - New section for Passed|Blocked UNCHECKED + - Change summary output criteria to check for any non-zero Totals + +2007-02-17 + - Revision 1.43 2007/02/17 18:43:39 mrc + - Ensure no output occurs when nothing is captured + - Sync up the shared routines with the postfix code + - Made maximum report width configurable in amavis.conf + - All lines in report now obey max report width + - Warn about dccproc unable to open map error + +2007-02-16 + - Revision 1.42 2007/02/16 06:21:20 mrc + +2007-02-11 + - Capture and summarize Bad Address Syntax message + (in postfix, can occur when strict_rfc821_envelopes is not set) + +2007-02-08 + - Group similar BAD HEADER messages and remove From key + +2007-02-07 + - Revision 1.41 2007/02/07 04:19:43 mrc + +2007-02-05 + - Update IPv6 RE to accept amavis' "IPv6:" prefix + - Liberalize several REs to better capture policy bank names + - Ignore lines: + "OS_fingerprint:" + "run_as_subprocess: child process... " + "Sophie broken pipe ..." + "penpals: (bonus|prev Subject:|this Subject:" + "virus_scan: \(bad jpeg: Invalid marker segm len" + - Ignore lines from upcoming 2.5.0 version: + "adding SA score \S+ to existing" + "Turning AV infection into a spam report:" + - Correct bad syntax of "$#{@$aref}" with "$#$aref" + - Enable "use strict" + - Declare global vars + - Thanks: Mark Martinec + +2007-02-02 + - Fixed Banned RE, which would fail to capture a banned filename + that contained an right paren. + +2007-02-02 + - Revision 1.40 2007/02/02 18:41:09 mrc + +2007-02-01 + - Revision 1.39 2007/02/01 20:16:52 mrc + - Removed inadvertent debug prints, and extra newline from timing reports. + +2007-02-01 + - Revision 1.38 2007/02/01 19:58:50 mrc + - Changed amavis timing report to display percentiles + instead of mean. Percentiles are user configurable. + Suggested by: Mark Martinec + +2007-02-01 + - Removed Min/Max columns, and replaced them instead + by adding 0 and 100 percentiles to the default list. + - Allow for multiple continuation lines + - Fixed issue with unmatched lines that contain a % + +2007-01-31 + - Revision 1.37 2007/01/31 06:38:43 mrc + - Minor updates for amavis 2.4.5 log messages changes + - Capture and summarize panic messages in new Panic section + +2007-01-30 + - Revision 1.36 2007/01/31 02:50:15 mrc + - Provide amavis timing report (detail >= 5) + - Correctly capture amavis log continuation lines + - Capture additional ClamAV warnings + +2007-01-27 + - Revision 1.35 2007/01/27 20:29:21 mrc + - Pin important errors to top of report + - Handle IPv6 addresses + - Thanks: Harald Geiger, Geert Janssens, Leon Kolchinsky, Mark Richards + +2007-01-26 + - Add Recipient key in Malware Blocked section + - Lowercase Recipient email addresses + +2007-01-24 + - Added Malware by Scanner section + +2007-01-23 + - Capture and summarize: + All "(!+)..." debug lines, placed in warning section + DSN Notification debug lines + +2007-01-22 + - Capture and summarize: + "(!)WARN: ..." lines + "INFO: truncated N header line(s) longer than 998 characters" + - Ignore "...email.txt no longer exists, can't re-use it" lines + +2007-01-19 + - Use same tree-building and reporting code as postfix filter + - Detailed summary lines are sorted first by count, then by IP and lexically + - Provide ability to configure per section maximum detail + - Added bytes scanned summary + - Ignore additional log lines: + "Waiting for the process [NNN] to terminate" + "do_notify_and_quarantine: rec_ccat" + "Valid PID file (younger than sys uptime ..." + "Sending SIGxxx to amavisd" + "Daemon [NNN] terminated by SIG..." + - Capture and report on missed or ignored log lines + additional "SA TIMED OUT" messages + A/V timeouts + encrypted archive members + "logging initialized, log level N, syslog: amavis.mail" + - Make "extra code modules loaded" report consistent with others + - Spam discarded (not quarantined) percentage is now shown as + percentage of Total scanned instead of Spam blocked + +# old logwatch amavis CVS log + +Revision 1.34 2006/11/12 18:39:34 bjorn +Change by MrC: +- Catches more amavis continuation lines +- Reports on mail released from quarantine via "amavisd-release" +- Reports supplemental minor bad headers available in amavisd 2.4.4+ +- Suppresses empty detail lines in generic doReport routine + +Revision 1.33 2006/10/16 22:19:05 mike +Amavis patch for protocols from Mike Cappella -mgt + +Revision 1.32 2006/07/06 00:25:00 bjorn +Additional filtering and counting, by Geert Janssens + +Revision 1.31 2006/06/29 16:02:05 bjorn +Corrected two previous log entries. + +Revision 1.30 2006/06/27 15:17:53 bjorn +Improved parsing and counting from Geert Janssens. + +Revision 1.29 2006/06/24 16:06:12 bjorn +Major rewrite from Mike Cappella. + +Revision 1.28 2006/05/26 18:32:50 bjorn +Additional regexp adjustment, by 'Who Knows'. + +Revision 1.27 2006/05/21 18:17:41 bjorn +Complies with recent amavis releases, by Who Knows. + +Revision 1.26 2006/04/02 17:26:52 kirk +fixed spelling error + +Revision 1.25 2006/01/29 23:52:53 bjorn +Print out virus names and sender, by Felix Schwarz. + +Revision 1.24 2005/12/07 19:15:56 bjorn +Detect and count timeouts, by 'Who Knows'. + +Revision 1.23 2005/11/30 05:34:10 bjorn +Corrected regexp with space, by Markus Lude. + +Revision 1.22 2005/11/22 18:34:32 bjorn +Recognize 'Passed' bad headers, by "Who Knows". + +Revision 1.21 2005/10/26 05:40:43 bjorn +Additional patches for amavisd-new, by Mike Cappella diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1be1fb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +########################################################################## +# Amavis-logwatch: written and maintained by: +# +# Mike "MrC" Cappella +# http://logreporters.sourceforge.net/ +# +# Please send all comments, suggestions, bug reports regarding this +# program/module to the email address above. I will respond as quickly +# as possible. [MrC] +# +####################################################### +### All work since Dec 12, 2006 (logwatch CVS revision 1.28) +### Copyright (c) 2006-2012 Mike Cappella +### +### Covered under the included MIT/X-Consortium License: +### http://www.opensource.org/licenses/mit-license.php +########################################################################## diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f5d7b78 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +program = amavis-logwatch +program_logwatch = amavis + +src = $(program) \ + $(program).conf \ + $(program).1 \ + $(program).1.html \ + Changes Bugs Makefile README LICENSE + +prefix_logwatch = /etc/logwatch +prefix_standalone = /usr/local + +scriptdir_logwatch = $(prefix_logwatch)/scripts/services +confdir_logwatch = $(prefix_logwatch)/conf/services + +scriptdir_standalone = $(prefix_standalone)/bin +confdir_standalone = $(prefix_standalone)/etc +mandir_standalone = $(prefix_standalone)/man/man1 + +toolsdir = ../tools + +INSTALL = /usr/bin/install -c + +install: + @echo 'Run "make install-logwatch" to install as a logwatch filter' + @echo 'Run "make install-standalone" to install as a standalone utility' + @echo 'Run "make install-all" to install both' + +install-logwatch: + $(INSTALL) -d -m 0755 $(DESTDIR)$(scriptdir_logwatch) $(DESTDIR)$(confdir_logwatch) || exit 1; + $(INSTALL) -m 0644 $(program) $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch) || exit 1; + $(INSTALL) -m 0644 $(program).conf $(DESTDIR)$(confdir_logwatch)/$(program_logwatch).conf || exit 1; + # removes taint mode (-T) to run under logwatch + perl -e 'while (<>) { $$.==1 and s/\s+-T$$//; print "$$_";}' -i $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch) + +install-standalone: + $(INSTALL) -d -m 0755 $(DESTDIR)$(scriptdir_standalone) $(DESTDIR)$(confdir_standalone) || exit 1; + $(INSTALL) -m 0755 $(program) $(DESTDIR)$(scriptdir_standalone)/$(program) || exit 1; + $(INSTALL) -m 0644 $(program).conf $(DESTDIR)$(confdir_standalone)/$(program).conf || exit 1; + $(INSTALL) -m 0644 $(program).1 $(DESTDIR)$(mandir_standalone)/$(program).1 || exit 1; + +install-all: install-logwatch install-standalone + +uninstall-logwatch: + -rm $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch) $(DESTDIR)$(confdir_logwatch)/$(program_logwatch).conf + +uninstall-standalone: + -rm $(DESTDIR)$(scriptdir_standalone)/$(program) $(DESTDIR)$(confdir_standalone)/$(program).conf \ + $(DESTDIR)$(mandir_standalone)/$(program).1 + +uninstall-all: uninstall-logwatch uninstall-standalone + +release: program htmlpage + vers=`egrep 'Version[ ]*=' $(program) | sed "s/.*'\(.*\)';/\1/"` ; \ + echo Preparing version $$vers; \ + rel=$(program)-$$vers ; \ + tar -czvf $${rel}.tgz --group=0 --owner=0 --mode=644 --transform=s",^,$${rel}/," $(src) ; \ + md5sum $${rel}.tgz > $${rel}.tgz.md5 ; \ + chmod 644 $${rel}.tgz $${rel}.tgz.md5 + +program: + $(toolsdir)/build_from_modules $(program) >| $(program); + +htmlpage: + groff -m mandoc -T ascii $(program).1 | $(toolsdir)/man2html -t 'Man page: $(program)(1)' >| $(program).1.html; diff --git a/README b/README new file mode 100644 index 0000000..b9e549d --- /dev/null +++ b/README @@ -0,0 +1,90 @@ +##### Overview + +The amavis-logwatch(1) utility is an Amavisd-new log parser, that pro- +duces summaries, details, and statistics regarding the operation of +Amavisd-new. + +This utility can be used as a standalone program, or as a Logwatch fil- +ter module to produce Amavis summary and detailed reports from within +Logwatch. + +A key feature of amavis-logwatch is its ability to produce a very wide +range of reports with data grouped and sorted as much as possible to +reduce noise and highlight patterns. Brief summary reports provide a +quick overview of general Amavis operations and message scanning, +calling out warnings that may require attention. Detailed reports pro- +vide easy to scan, hierarchically-arranged and organized information, +with as much or little detail as desired. + +##### Installation: standalone + +The included Makefile will install the amavis-logwatch utility +for you. Run: + + make install-standalone + +to install the utility and its configuration file. Installation +will default to /usr/local/bin and /usr/local/etc, respectively. + +##### Installation: logwatch + +To use amavis-logwatch as a logwatch script, the files: + + amavis-logwatch + amavis-logwatch.conf + +will need to be installed into one of logwatch's known directories. +To avoid overwriting your existing default logwatch filter files, +the enclosed files can be installed into the global logwatch +installation directory, which is typically: + + /etc/logwatch + +The included Makefile can be used to install the files into +/etc/logwatch for you. To install the filter into an existing +logwatch installation, run: + + make install-logwatch + +NOTE: the files amavis-logwatch and amavis-logwatch.conf must be +renamed (by removing the "-logwatch" suffix), for logwatch to +function correctly. The Makefile takes care of this. The +Makefile also disables perl's taint mode (-T) when running under +logwatch. If you install the files manually, be sure to remove +the -T from the first line of the amavis filter. + +For non-standard installations, you will need to determine your +global logwatch directory, and define "prefix" in the attached +Makefile. + +Alternatively, you can manually copy the files to their proper +locations: + + cp amavis-logwatch /etc/logwatch/scripts/services/amavis + cp amavis-logwatch.conf /etc/logwatch/conf/services/amavis.conf + [ remove -T from line 1 of /etc/logwatch/scripts/services/amavis ] + + Optional: + cp amavis-logwatch.1 /usr/local/man/man1/amavis-logwatch.1 + +##### Usage + +The amavis-logwatch utility is used standalone as: + + amavis-logwatch /path/to/amavislog + +For brief help: + + amavis-logwatch --help + +To use within logwatch: + + logwatch --service amavis ... + +See the amavis-logwatch(1) man page for complete details, and +see comments in the postfix-logwatch.conf file for additional +information. + +Mike Cappella +mike (at) cappella (dot) us +last updated: 01/11/2012 diff --git a/amavis-logwatch b/amavis-logwatch new file mode 100644 index 0000000..4ad07f6 --- /dev/null +++ b/amavis-logwatch @@ -0,0 +1,4067 @@ +#!/usr/bin/perl -T + +########################################################################## +# Amavis-logwatch: written and maintained by: +# +# Mike "MrC" Cappella +# http://logreporters.sourceforge.net/ +# +# Please send all comments, suggestions, bug reports regarding this +# program/module to the email address above. I will respond as quickly +# as possible. [MrC] +# +# Questions regarding the logwatch program itself should be directed to +# the logwatch project at: +# http://sourceforge.net/projects/logwatch/support +# +####################################################### +### All work since Dec 12, 2006 (logwatch CVS revision 1.28) +### Copyright (c) 2006-2012 Mike Cappella +### +### Covered under the included MIT/X-Consortium License: +### http://www.opensource.org/licenses/mit-license.php +### All modifications and contributions by other persons to +### this script are assumed to have been donated to the +### Logwatch project and thus assume the above copyright +### and licensing terms. If you want to make contributions +### under your own copyright or a different license this +### must be explicitly stated in the contribution an the +### Logwatch project reserves the right to not accept such +### contributions. If you have made significant +### contributions to this script and want to claim +### copyright please contact logwatch-devel@lists.sourceforge.net. +########################################################## + +########################################################################## +# The original amavis logwatch filter was written by +# Jim O'Halloran , and has had many contributors over +# the years. +# +# CVS log removed: see Changes file for amavis-logwatch at +# http://logreporters.sourceforge.net/ +# or included with the standalone amavis-logwatch distribution +########################################################################## + +package Logreporters; +use 5.008; +use strict; +use warnings; +no warnings "uninitialized"; +use re 'taint'; + +our $Version = '1.51.03'; +our $progname_prefix = 'amavis'; + +# Specifies the default configuration file for use in standalone mode. +my $config_file = "/usr/local/etc/${progname_prefix}-logwatch.conf"; + +#MODULE: ../Logreporters/Utils.pm +package Logreporters::Utils; + +use 5.008; +use strict; +use re 'taint'; +use warnings; + +BEGIN { + use Exporter (); + use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); + $VERSION = '1.003'; + @ISA = qw(Exporter); + @EXPORT = qw(&formathost &get_percentiles &get_percentiles2 &get_frequencies &commify &unitize + &get_usable_sectvars &add_section &begin_section_group &end_section_group + &get_version &unique_list); + @EXPORT_OK = qw(&gen_test_log); +} + +use subs qw (@EXPORT @EXPORT_OK); + + +# Formats IP and hostname for even column spacing +# +sub formathost($ $) { + # $_[0] : hostip + # $_[1] : hostname; + + if (! $Logreporters::Config::Opts{'unknown'} and $_[1] eq 'unknown') { + return $_[0]; + } + + return sprintf "%-$Logreporters::Config::Opts{'ipaddr_width'}s %s", + $_[0] eq '' ? '*unknown' : $_[0], + $_[1] eq '' ? '*unknown' : lc $_[1]; +} + +# Add a new section to the end of a section table +# +sub add_section($$$$$;$) { + my $sref = shift; + die "Improperly specified Section entry: $_[0]" if !defined $_[3]; + + my $entry = { + CLASS => 'DATA', + NAME => $_[0], + DETAIL => $_[1], + FMT => $_[2], + TITLE => $_[3], + }; + $entry->{'DIVISOR'} = $_[4] if defined $_[4]; + push @$sref, $entry; +} + +{ +my $group_level = 0; + +# Begin a new section group. Groups can nest. +# +sub begin_section_group($;@) { + my $sref = shift; + my $group_name = shift; + my $entry = { + CLASS => 'GROUP_BEGIN', + NAME => $group_name, + LEVEL => ++$group_level, + HEADERS => [ @_ ], + }; + push @$sref, $entry; +} + +# Ends a section group. +# +sub end_section_group($;@) { + my $sref = shift; + my $group_name = shift; + my $entry = { + CLASS => 'GROUP_END', + NAME => $group_name, + LEVEL => --$group_level, + FOOTERS => [ @_ ], + }; + push @$sref, $entry; +} +} + +# Generate and return a list of section table entries or +# limiter key names, skipping any formatting entries. +# If 'namesonly' is set, limiter key names are returned, +# otherwise an array of section array records is returned. +sub get_usable_sectvars(\@ $) { + my ($sectref,$namesonly) = @_; + my (@sect_list, %unique_names); + + foreach my $sref (@$sectref) { + #print "get_usable_sectvars: $sref->{NAME}\n"; + next unless $sref->{CLASS} eq 'DATA'; + if ($namesonly) { + $unique_names{$sref->{NAME}} = 1; + } + else { + push @sect_list, $sref; + } + } + # return list of unique names + if ($namesonly) { + return keys %unique_names; + } + return @sect_list; +} + +# Print program and version info, preceeded by an optional string, and exit. +# +sub get_version() { + + print STDOUT "@_\n" if ($_[0]); + print STDOUT "$Logreporters::progname: $Logreporters::Version\n"; + exit 0; +} + + +# Returns a list of percentile values given a +# sorted array of numeric values. Uses the formula: +# +# r = 1 + (p(n-1)/100) = i + d (Excel method) +# +# r = rank +# p = desired percentile +# n = number of items +# i = integer part +# d = decimal part +# +# Arg1 is an array ref to the sorted series +# Arg2 is a list of percentiles to use + +sub get_percentiles(\@ @) { + my ($aref,@plist) = @_; + my ($n, $last, $r, $d, $i, @vals, $Yp); + + $last = $#$aref; + $n = $last + 1; + #printf "%6d" x $n . "\n", @{$aref}; + + #printf "n: %4d, last: %d\n", $n, $last; + foreach my $p (@plist) { + $r = 1 + ($p * ($n - 1) / 100.0); + $i = int ($r); # integer part + # domain: $i = 1 .. n + if ($i == $n) { + $Yp = $aref->[$last]; + } + elsif ($i == 0) { + $Yp = $aref->[0]; + print "CAN'T HAPPEN: $Yp\n"; + } + else { + $d = $r - $i; # decimal part + #p = Y[i] + d(Y[i+1] - Y[i]), but since we're 0 based, use i=i-1 + $Yp = $aref->[$i-1] + ($d * ($aref->[$i] - $aref->[$i-1])); + } + #printf "\np(%6.2f), r: %6.2f, i: %6d, d: %6.2f, Yp: %6d", $p, $r, $i, $d, $Yp; + push @vals, $Yp; + } + + return @vals; +} + +sub get_num_scores($) { + my $scoretab_r = shift; + + my $totalscores = 0; + + for (my $i = 0; $i < @$scoretab_r; $i += 2) { + $totalscores += $scoretab_r->[$i+1] + } + + return $totalscores; +} + +# scoretab +# +# (score1, n1), (score2, n2), ... (scoreN, nN) +# $i $i+1 +# +# scores are 0 based (0 = 1st score) +sub get_nth_score($ $) { + my ($scoretab_r, $n) = @_; + + my $i = 0; + my $n_cur_scores = 0; + #print "Byscore (", .5 * @$scoretab_r, "): "; for (my $i = 0; $i < $#$scoretab_r / 2; $i++) { printf "%9s (%d) ", $scoretab_r->[$i], $scoretab_r->[$i+1]; } ; print "\n"; + + while ($i < $#$scoretab_r) { + #print "Samples_seen: $n_cur_scores\n"; + $n_cur_scores += $scoretab_r->[$i+1]; + if ($n_cur_scores >= $n) { + #printf "range: %s %s %s\n", $i >= 2 ? $scoretab_r->[$i - 2] : '', $scoretab_r->[$i], $i+2 > $#$scoretab_r ? '' : $scoretab_r->[$i + 2]; + #printf "n: $n, i: %8d, n_cur_scores: %8d, score: %d x %d hits\n", $i, $n_cur_scores, $scoretab_r->[$i], $scoretab_r->[$i+1]; + return $scoretab_r->[$i]; + } + + $i += 2; + } + print "returning last score $scoretab_r->[$i]\n"; + return $scoretab_r->[$i]; +} + +sub get_percentiles2(\@ @) { + my ($scoretab_r, @plist) = @_; + my ($n, $last, $r, $d, $i, @vals, $Yp); + + #$last = $#$scoretab_r - 1; + $n = get_num_scores($scoretab_r); + #printf "\n%6d" x $n . "\n", @{$scoretab_r}; + + #printf "\n\tn: %4d, @$scoretab_r\n", $n; + foreach my $p (@plist) { + ###print "\nPERCENTILE: $p\n"; + $r = 1 + ($p * ($n - 1) / 100.0); + $i = int ($r); # integer part + if ($i == $n) { + #print "last:\n"; + #$Yp = $scoretab_r->[$last]; + $Yp = get_nth_score($scoretab_r, $n); + } + elsif ($i == 0) { + #$Yp = $scoretab_r->[0]; + print "1st: CAN'T HAPPEN\n"; + $Yp = get_nth_score($scoretab_r, 1); + } + else { + $d = $r - $i; # decimal part + #p = Y[i] + d(Y[i+1] - Y[i]), but since we're 0 based, use i=i-1 + my $ithvalprev = get_nth_score($scoretab_r, $i); + my $ithval = get_nth_score($scoretab_r, $i+1); + $Yp = $ithvalprev + ($d * ($ithval - $ithvalprev)); + } + #printf "p(%6.2f), r: %6.2f, i: %6d, d: %6.2f, Yp: %6d\n", $p, $r, $i, $d, $Yp; + push @vals, $Yp; + } + + return @vals; +} + + + +# Returns a list of frequency distributions given an incrementally sorted +# set of sorted scores, and an incrementally sorted list of buckets +# +# Arg1 is an array ref to the sorted series +# Arg2 is a list of frequency buckets to use +sub get_frequencies(\@ @) { + my ($aref,@blist) = @_; + + my @vals = ( 0 ) x (@blist); + my @sorted_blist = sort { $a <=> $b } @blist; + my $bucket_index = 0; + +OUTER: foreach my $score (@$aref) { + #print "Score: $score\n"; + for my $i ($bucket_index .. @sorted_blist - 1) { + #print "\tTrying Bucket[$i]: $sorted_blist[$i]\n"; + if ($score > $sorted_blist[$i]) { + $bucket_index++; + } + else { + #printf "\t\tinto Bucket[%d]\n", $bucket_index; + $vals[$bucket_index]++; + next OUTER; + } + } + #printf "\t\tinto Bucket[%d]\n", $bucket_index - 1; + $vals[$bucket_index - 1]++; + } + + return @vals; +} + +# Inserts commas in numbers for easier readability +# +sub commify ($) { + return undef if ! defined ($_[0]); + + my $text = reverse $_[0]; + $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; + return scalar reverse $text; +} + +# Unitize a number, and return appropriate printf formatting string +# +sub unitize($ $) { + my ($num, $fmt) = @_; + my $kilobyte = 2**10; + my $megabyte = 2**20; + my $gigabyte = 2**30; + my $terabyte = 2**40; + + if ($num >= $terabyte) { + $num /= $terabyte; + $fmt .= '.3fT'; + } elsif ($num >= $gigabyte) { + $num /= $gigabyte; + $fmt .= '.3fG'; + } elsif ($num >= $megabyte) { + $num /= $megabyte; + $fmt .= '.3fM'; + } elsif ($num >= $kilobyte) { + $num /= $kilobyte; + $fmt .= '.3fK'; + } else { + $fmt .= 'd '; + } + + return ($num, $fmt); +} + +# Returns a sublist of the supplied list of elements in an unchanged order, +# where only the first occurrence of each defined element is retained +# and duplicates removed +# +# Borrowed from amavis 2.6.2 +# +sub unique_list(@) { + my ($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref + my (%seen); + my (@unique) = grep { defined($_) && !$seen{$_}++ } @$r; + + return @unique; +} + +# Generate a test maillog file from the '#TD' test data lines +# The test data file is placed in /var/tmp/maillog.autogen +# +# arg1: "postfix" or "amavis" +# arg2: path to postfix-logwatch or amavis-logwatch from which to read '#TD' data +# +# Postfix TD syntax: +# TD() log entry +# +sub gen_test_log($) { + my $scriptpath = shift; + + my $toolname = $Logreporters::progname_prefix; + my $datafile = "/var/tmp/maillog-${toolname}.autogen"; + + die "gen_test_log: invalid toolname $toolname" if ($toolname !~ /^(postfix|amavis)$/); + + eval { + require Sys::Hostname; + require Fcntl; + } or die "Unable to create test data file: required module(s) not found\n$@"; + + my $syslogtime = localtime; + $syslogtime =~ s/^....(.*) \d{4}$/$1/; + + my ($hostname) = split /\./, Sys::Hostname::hostname(); + + # # avoid -T issues + # delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + + my $flags = &Fcntl::O_CREAT|&Fcntl::O_WRONLY|&Fcntl::O_TRUNC; + sysopen(FH, $datafile, $flags) or die "Can't create test data file: $!"; + print "Generating test log data file from $scriptpath: $datafile\n"; + + my $id; + @ARGV = ($scriptpath); + if ($toolname eq 'postfix') { + my %services = ( + DEF => 'smtpd', + bQ => 'bounce', + cN => 'cleanup', + cQ => 'cleanup', + lQ => 'local', + m => 'master', + p => 'pickup', + pQ => 'pickup', + ppQ => 'pipe', + pfw => 'postfwd', + pg => 'postgrey', + pgQ => 'postgrey', + ps => 'postsuper', + qQ => 'qmgr', + s => 'smtp', + sQ => 'smtp', + sd => 'smtpd', + sdN => 'smtpd', + sdQ => 'smtpd', + spf => 'policy-spf', + vN => 'virtual', + vQ => 'virtual', + ); + $id = 'postfix/smtp[12345]'; + + while (<>) { + if (/^\s*#TD([a-zA-Z]*[NQ]?)(\d+)?(?:\(([^)]+)\))? (.*)$/) { + my ($service,$count,$qid,$line) = ($1, $2, $3, $4); + + #print "SERVICE: %s, QID: %s, COUNT: %s, line: %s\n", $service, $qid, $count, $line; + + if ($service eq '') { + $service = 'DEF'; + } + die ("No such service: \"$service\": line \"$_\"") if (!exists $services{$service}); + + $id = $services{$service} . '[123]'; + $id = 'postfix/' . $id unless $services{$service} eq 'postgrey'; + #print "searching for service: \"$service\"\n\tFound $id\n"; + if ($service =~ /N$/) { $id .= ': NOQUEUE'; } + elsif ($service =~ /Q$/) { $id .= $qid ? $qid : ': DEADBEEF'; } + + $line =~ s/ +/ /g; + $line =~ s/^ //g; + #print "$syslogtime $hostname $id: \"$line\"\n" x ($count ? $count : 1); + print FH "$syslogtime $hostname $id: $line\n" x ($count ? $count : 1); + } + } + } + else { #amavis + my %services = ( + DEF => 'amavis', + dcc => 'dccproc', + ); + while (<>) { + if (/^\s*#TD([a-z]*)(\d+)? (.*)$/) { + my ($service,$count,$line) = ($1, $2, $3); + if ($service eq '') { + $service = 'DEF'; + } + die ("No such service: \"$service\": line \"$_\"") if (!exists $services{$service}); + $id = $services{$service} . '[123]:'; + if ($services{$service} eq 'amavis') { + $id .= ' (9999-99)'; + } + print FH "$syslogtime $hostname $id $line\n" x ($count ? $count : 1) + } + } + } + + close FH or die "Can't close $datafile: $!"; +} + +1; + +#MODULE: ../Logreporters/Config.pm +package Logreporters::Config; + +use 5.008; +use strict; +use re 'taint'; +use warnings; + + +BEGIN { + use Exporter (); + use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); + $VERSION = '1.002'; + @ISA = qw(Exporter); + @EXPORT = qw(&init_run_mode &add_option &get_options &init_cmdline &get_vars_from_file + &process_limiters &process_debug_opts &init_getopts_table_common &zero_opts + @Optspec %Opts %Configvars @Limiters %line_styles $fw1 $fw2 $sep1 $sep2 + &D_CONFIG &D_ARGS &D_VARS &D_TREE &D_SECT &D_UNMATCHED &D_TEST &D_ALL + ); +} + +use subs @EXPORT; + +our @Optspec = (); # options table used by Getopts + +our %Opts = (); # program-wide options +our %Configvars = (); # configuration file variables +our @Limiters; + +# Report separator characters and widths +our ($fw1,$fw2) = (22, 10); +our ($sep1,$sep2) = ('=', '-'); + +use Getopt::Long; + + +BEGIN { + import Logreporters::Utils qw(&get_usable_sectvars); +} + +our %line_styles = ( + truncate => 0, + wrap => 1, + full => 2, +); + +sub init_run_mode($); +sub confighash_to_cmdline(\%); +sub get_vars_from_file(\% $); +sub process_limiters(\@); +sub add_option(@); +sub get_options($); +sub init_getopts_table_common(@); +sub set_supplemental_reports($$); +# debug constants +sub D_CONFIG () { 1<<0 } +sub D_ARGS () { 1<<1 } +sub D_VARS () { 1<<2 } +sub D_TREE () { 1<<3 } +sub D_SECT () { 1<<4 } +sub D_UNMATCHED () { 1<<5 } + +sub D_TEST () { 1<<30 } +sub D_ALL () { 1<<31 } + +my %debug_words = ( + config => D_CONFIG, + args => D_ARGS, + vars => D_VARS, + tree => D_TREE, + sect => D_SECT, + unmatched => D_UNMATCHED, + + test => D_TEST, + all => 0xffffffff, +); + +# Clears %Opts hash and initializes basic running mode options in +# %Opts hash by setting keys: 'standalone', 'detail', and 'debug'. +# Call early. +# +sub init_run_mode($) { + my $config_file = shift; + $Opts{'debug'} = 0; + + # Logwatch passes a filter's options via environment variables. + # When running standalone (w/out logwatch), use command line options + $Opts{'standalone'} = exists ($ENV{LOGWATCH_DETAIL_LEVEL}) ? 0 : 1; + + # Show summary section by default + $Opts{'summary'} = 1; + + if ($Opts{'standalone'}) { + process_debug_opts($ENV{'LOGREPORTERS_DEBUG'}) if exists ($ENV{'LOGREPORTERS_DEBUG'}); + } + else { + $Opts{'detail'} = $ENV{'LOGWATCH_DETAIL_LEVEL'}; + # XXX + #process_debug_opts($ENV{'LOGWATCH_DEBUG'}) if exists ($ENV{'LOGWATCH_DEBUG'}); + } + + # first process --debug, --help, and --version options + add_option ('debug=s', sub { process_debug_opts($_[1]); 1}); + add_option ('version', sub { &Logreporters::Utils::get_version(); 1;}); + get_options(1); + + # now process --config_file, so that all config file vars are read first + add_option ('config_file|f=s', sub { get_vars_from_file(%Configvars, $_[1]); 1;}); + get_options(1); + + # if no config file vars were read + if ($Opts{'standalone'} and ! keys(%Configvars) and -f $config_file) { + print "Using default config file: $config_file\n" if $Opts{'debug'} & D_CONFIG; + get_vars_from_file(%Configvars, $config_file); + } +} + +sub get_options($) { + my $pass_through = shift; + #$SIG{__WARN__} = sub { print "*** $_[0]*** options error\n" }; + # ensure we're called after %Opts is initialized + die "get_options: program error: %Opts is emtpy" unless exists $Opts{'debug'}; + + my $p = new Getopt::Long::Parser; + + if ($pass_through) { + $p->configure(qw(pass_through permute)); + } + else { + $p->configure(qw(no_pass_through no_permute)); + } + #$p->configure(qw(debug)); + + if ($Opts{'debug'} & D_ARGS) { + print "\nget_options($pass_through): enter\n"; + printf "\tARGV(%d): ", scalar @ARGV; + print @ARGV, "\n"; + print "\t$_ ", defined $Opts{$_} ? "=> $Opts{$_}\n" : "\n" foreach sort keys %Opts; + } + + if ($p->getoptions(\%Opts, @Optspec) == 0) { + print STDERR "Use ${Logreporters::progname} --help for options\n"; + exit 1; + } + if ($Opts{'debug'} & D_ARGS) { + print "\t$_ ", defined $Opts{$_} ? "=> $Opts{$_}\n" : "\n" foreach sort keys %Opts; + printf "\tARGV(%d): ", scalar @ARGV; + print @ARGV, "\n"; + print "get_options: exit\n"; + } +} + +sub add_option(@) { + push @Optspec, @_; +} + +# untaint string, borrowed from amavisd-new +sub untaint($) { + no re 'taint'; + + my ($str); + if (defined($_[0])) { + local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness + $str = $1 if $_[0] =~ /^(.*)$/; + } + + return $str; +} + +sub init_getopts_table_common(@) { + my @supplemental_reports = @_; + + print "init_getopts_table_common: enter\n" if $Opts{'debug'} & D_ARGS; + + add_option ('help', sub { print STDOUT Logreporters::usage(undef); exit 0 }); + add_option ('gen_test_log=s', sub { Logreporters::Utils::gen_test_log($_[1]); exit 0; }); + add_option ('detail=i'); + add_option ('nodetail', sub { + # __none__ will set all limiters to 0 in process_limiters + # since they are not known (Sections table is not yet built). + push @Limiters, '__none__'; + # 0 = disable supplemental_reports + set_supplemental_reports(0, \@supplemental_reports); + }); + add_option ('max_report_width=i'); + add_option ('summary!'); + add_option ('show_summary=i', sub { $Opts{'summary'} = $_[1]; 1; }); + # untaint ipaddr_width for use w/sprintf() in Perl v5.10 + add_option ('ipaddr_width=i', sub { $Opts{'ipaddr_width'} = untaint ($_[1]); 1; }); + + add_option ('sect_vars!'); + add_option ('show_sect_vars=i', sub { $Opts{'sect_vars'} = $_[1]; 1; }); + + add_option ('syslog_name=s'); + add_option ('wrap', sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; }); + add_option ('full', sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; }); + add_option ('truncate', sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; }); + add_option ('line_style=s', sub { + my $style = lc($_[1]); + my @list = grep (/^$style/, keys %line_styles); + if (! @list) { + print STDERR "Invalid line_style argument \"$_[1]\"\n"; + print STDERR "Option line_style argument must be one of \"wrap\", \"full\", or \"truncate\".\n"; + print STDERR "Use $Logreporters::progname --help for options\n"; + exit 1; + } + $Opts{'line_style'} = $line_styles{lc($list[0])}; + 1; + }); + + add_option ('limit|l=s', sub { + my ($limiter,$lspec) = split(/=/, $_[1]); + if (!defined $lspec) { + printf STDERR "Limiter \"%s\" requires value (ex. --limit %s=10)\n", $_[1],$_[1]; + exit 2; + } + foreach my $val (split(/(?:\s+|\s*,\s*)/, $lspec)) { + if ($val !~ /^\d+$/ and + $val !~ /^(\d*)\.(\d+)$/ and + $val !~ /^::(\d+)$/ and + $val !~ /^:(\d+):(\d+)?$/ and + $val !~ /^(\d+):(\d+)?:(\d+)?$/) + { + printf STDERR "Limiter value \"$val\" invalid in \"$limiter=$lspec\"\n"; + exit 2; + } + } + push @Limiters, lc $_[1]; + }); + + print "init_getopts_table_common: exit\n" if $Opts{'debug'} & D_ARGS; +} + +sub get_option_names() { + my (@ret, @tmp); + foreach (@Optspec) { + if (ref($_) eq '') { # process only the option names + my $spec = $_; + $spec =~ s/=.*$//; + $spec =~ s/([^|]+)\!$/$1|no$1/g; + @tmp = split /[|]/, $spec; + #print "PUSHING: @tmp\n"; + push @ret, @tmp; + } + } + return @ret; +} + +# Set values for the configuration variables passed via hashref. +# Variables are of the form ${progname_prefix}_KEYNAME. +# +# Because logwatch lowercases all config file entries, KEYNAME is +# case-insensitive. +# +sub init_cmdline() { + my ($href, $configvar, $value, $var); + + # logwatch passes all config vars via environment variables + $href = $Opts{'standalone'} ? \%Configvars : \%ENV; + + # XXX: this is cheeze: need a list of valid limiters, but since + # the Sections table is not built yet, we don't know what is + # a limiter and what is an option, as there is no distinction in + # variable names in the config file (perhaps this should be changed). + my @valid_option_names = get_option_names(); + die "Options table not yet set" if ! scalar @valid_option_names; + + print "confighash_to_cmdline: @valid_option_names\n" if $Opts{'debug'} & D_ARGS; + my @cmdline = (); + while (($configvar, $value) = each %$href) { + if ($configvar =~ s/^${Logreporters::progname_prefix}_//o) { + # distinguish level limiters from general options + # would be easier if limiters had a unique prefix + $configvar = lc $configvar; + my $ret = grep (/^$configvar$/i, @valid_option_names); + if ($ret == 0) { + print "\tLIMITER($ret): $configvar = $value\n" if $Opts{'debug'} & D_ARGS; + push @cmdline, '-l', "$configvar" . "=$value"; + } + else { + print "\tOPTION($ret): $configvar = $value\n" if $Opts{'debug'} & D_ARGS; + unshift @cmdline, $value if defined ($value); + unshift @cmdline, "--$configvar"; + } + } + } + unshift @ARGV, @cmdline; +} + +# Obtains the variables from a logwatch-style .conf file, for use +# in standalone mode. Returns an ENV-style hash of key/value pairs. +# +sub get_vars_from_file(\% $) { + my ($href, $file) = @_; + my ($var, $val); + + print "get_vars_from_file: enter: processing file: $file\n" if $Opts{'debug'} & D_CONFIG; + + my $message = undef; + my $ret = stat ($file); + if ($ret == 0) { $message = $!; } + elsif (! -r _) { $message = "Permission denied"; } + elsif ( -d _) { $message = "Is a directory"; } + elsif (! -f _) { $message = "Not a regular file"; } + + if ($message) { + print STDERR "Configuration file \"$file\": $message\n"; + exit 2; + } + + my $prog = $Logreporters::progname_prefix; + open FILE, '<', "$file" or die "unable to open configuration file $file: $!"; + while () { + chomp; + next if (/^\s*$/); # ignore all whitespace lines + next if (/^\*/); # ignore logwatch's *Service lines + next if (/^\s*#/); # ignore comment lines + if (/^\s*\$(${prog}_[^=\s]+)\s*=\s*"?([^"]+)"?$/o) { + ($var,$val) = ($1,$2); + if ($val =~ /^(?:no|false)$/i) { $val = 0; } + elsif ($val =~ /^(?:yes|true)$/i) { $val = 1; } + elsif ($val eq '') { $var =~ s/${prog}_/${prog}_no/; $val = undef; } + + print "\t\"$var\" => \"$val\"\n" if $Opts{'debug'} & D_CONFIG; + + $href->{$var} = $val; + } + } + close FILE or die "failed to close configuration handle for $file: $!"; + print "get_vars_from_file: exit\n" if $Opts{'debug'} & D_CONFIG; +} + +sub process_limiters(\@) { + my ($sectref) = @_; + + my ($limiter, $var, $val, @errors); + my @l = get_usable_sectvars(@$sectref, 1); + + if ($Opts{'debug'} & D_VARS) { + print "process_limiters: enter\n"; + print "\tLIMITERS: @Limiters\n"; + } + while ($limiter = shift @Limiters) { + my @matched = (); + + printf "\t%-30s ",$limiter if $Opts{'debug'} & D_VARS; + # disable all limiters when limiter is __none__: see 'nodetail' cmdline option + if ($limiter eq '__none__') { + $Opts{$_} = 0 foreach @l; + next; + } + + ($var,$val) = split /=/, $limiter; + + if ($val eq '') { + push @errors, "Limiter \"$var\" requires value (ex. --limit limiter=10)"; + next; + } + + # try exact match first, then abbreviated match next + if (scalar (@matched = grep(/^$var$/, @l)) == 1 or scalar (@matched = grep(/^$var/, @l)) == 1) { + $limiter = $matched[0]; # unabbreviate limiter + print "MATCH: $var: $limiter => $val\n" if $Opts{'debug'} & D_VARS; + # XXX move limiters into section hash entry... + $Opts{$limiter} = $val; + next; + } + print "matched=", scalar @matched, ": @matched\n" if $Opts{'debug'} & D_VARS; + + push @errors, "Limiter \"$var\" is " . (scalar @matched == 0 ? "invalid" : "ambiguous: @matched"); + } + print "\n" if $Opts{'debug'} & D_VARS; + + if (@errors) { + print STDERR "$_\n" foreach @errors; + exit 2; + } + + # Set the default value of 10 for each section if no limiter exists. + # This allows output for each section should there be no configuration + # file or missing limiter within the configuration file. + foreach (@l) { + $Opts{$_} = 10 unless exists $Opts{$_}; + } + + # Enable collection for each section if a limiter is non-zero. + foreach (@l) { + #print "L is: $_\n"; + #print "DETAIL: $Opts{'detail'}, OPTS: $Opts{$_}\n"; + $Logreporters::TreeData::Collecting{$_} = (($Opts{'detail'} >= 5) && $Opts{$_}) ? 1 : 0; + } + #print "OPTS: \n"; map { print "$_ => $Opts{$_}\n"} keys %Opts; + #print "COLLECTING: \n"; map { print "$_ => $Logreporters::TreeData::Collecting{$_}\n"} keys %Logreporters::TreeData::Collecting; +} + +# Enable/disable supplemental reports +# arg1: 0=off, 1=on +# arg2,...: list of supplemental report keywords +sub set_supplemental_reports($$) { + my ($onoff,$aref) = @_; + + $Opts{$_} = $onoff foreach (@$aref); +} + +sub process_debug_opts($) { + my $optstring = shift; + + my @errors = (); + foreach (split(/\s*,\s*/, $optstring)) { + my $word = lc $_; + my @matched = grep (/^$word/, keys %debug_words); + + if (scalar @matched == 1) { + $Opts{'debug'} |= $debug_words{$matched[0]}; + next; + } + + if (scalar @matched == 0) { + push @errors, "Unknown debug keyword \"$word\""; + } + else { # > 1 + push @errors, "Ambiguous debug keyword abbreviation \"$word\": (matches: @matched)"; + } + } + if (@errors) { + print STDERR "$_\n" foreach @errors; + print STDERR "Debug keywords: ", join (' ', sort keys %debug_words), "\n"; + exit 2; + } +} + +# Zero the options controlling level specs and those +# any others passed via Opts key. +# +# Zero the options controlling level specs in the +# Detailed section, and set all other report options +# to disabled. This makes it easy via command line to +# disable the entire summary section, and then re-enable +# one or more sections for specific reports. +# +# eg. progname --nodetail --limit forwarded=2 +# +sub zero_opts ($ @) { + my $sectref = shift; + # remaining args: list of Opts keys to zero + + map { $Opts{$_} = 0; print "zero_opts: $_ => 0\n" if $Opts{'debug'} & D_VARS;} @_; + map { $Opts{$_} = 0 } get_usable_sectvars(@$sectref, 1); +} + +1; + +#MODULE: ../Logreporters/TreeData.pm +package Logreporters::TreeData; + +use 5.008; +use strict; +use re 'taint'; +use warnings; +no warnings "uninitialized"; + +BEGIN { + use Exporter (); + use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); + $VERSION = '1.001'; + @ISA = qw(Exporter); + @EXPORT = qw(%Totals %Counts %Collecting $END_KEY); + @EXPORT_OK = qw(&printTree &buildTree); + +} + +use subs @EXPORT_OK; + +BEGIN { + import Logreporters::Config qw(%line_styles); +} + +# Totals and Counts are the log line accumulator hashes. +# Totals: maintains per-section grand total tallies for use in Summary section +# Counts: is a multi-level hash, which maintains per-level key totals. +our (%Totals, %Counts); + +# The Collecting hash determines which sections will be captured in +# the Counts hash. Counts are collected only if a section is enabled, +# and this hash obviates the need to test both existence and +# non-zero-ness of the Opts{'keyname'} (either of which cause capture). +# XXX The Opts hash could be used .... +our %Collecting = (); + +sub buildTree(\% $ $ $ $ $); +sub printTree($ $ $ $ $); +=pod +[ a:b:c, ... ] + +which would be interpreted as follows: + +a = show level a detail +b = show at most b items at this level +c = minimun count that will be shown +=cut + +sub printTree($ $ $ $ $) { + my ($treeref, $lspecsref, $line_style, $max_report_width, $debug) = @_; + my ($entry, $line); + my $cutlength = $max_report_width - 3; + + my $topn = 0; + foreach $entry (sort bycount @$treeref) { + ref($entry) ne "HASH" and die "Unexpected entry in tree: $entry\n"; + + #print "LEVEL: $entry->{LEVEL}, TOTAL: $entry->{TOTAL}, HASH: $entry, DATA: $entry->{DATA}\n"; + + # Once the top N lines have been printed, we're done + if ($lspecsref->[$entry->{LEVEL}]{topn}) { + if ($topn++ >= $lspecsref->[$entry->{LEVEL}]{topn} ) { + print ' ', ' ' x ($entry->{LEVEL} + 3), "...\n" + unless ($debug) and do { + $line = ' ' . ' ' x ($entry->{LEVEL} + 3) . '...'; + printf "%-130s L%d: topn reached(%d)\n", $line, $entry->{LEVEL} + 1, $lspecsref->[$entry->{LEVEL}]{topn}; + }; + last; + } + } + + # Once the item's count falls below the given threshold, we're done at this level + # unless a top N is specified, as threshold has lower priority than top N + elsif ($lspecsref->[$entry->{LEVEL}]{threshold}) { + if ($entry->{TOTAL} <= $lspecsref->[$entry->{LEVEL}]{threshold}) { + print ' ', ' ' x ($entry->{LEVEL} + 3), "...\n" + unless ($debug) and do { + $line = ' ' . (' ' x ($entry->{LEVEL} + 3)) . '...'; + printf "%-130s L%d: threshold reached(%d)\n", $line, $entry->{LEVEL} + 1, $lspecsref->[$entry->{LEVEL}]{threshold}; + }; + last; + } + } + + $line = sprintf "%8d%s%s", $entry->{TOTAL}, ' ' x ($entry->{LEVEL} + 2), $entry->{DATA}; + + if ($debug) { + printf "%-130s %-60s\n", $line, $entry->{DEBUG}; + } + + # line_style full, or lines < max_report_width + + #printf "MAX: $max_report_width, LEN: %d, CUTLEN $cutlength\n", length($line); + if ($line_style == $line_styles{'full'} or length($line) <= $max_report_width) { + print $line, "\n"; + } + elsif ($line_style == $line_styles{'truncate'}) { + print substr ($line,0,$cutlength), '...', "\n"; + } + elsif ($line_style == $line_styles{'wrap'}) { + my $leader = ' ' x 8 . ' ' x ($entry->{LEVEL} + 2); + print substr ($line, 0, $max_report_width, ''), "\n"; + while (length($line)) { + print $leader, substr ($line, 0, $max_report_width - length($leader), ''), "\n"; + } + } + else { + die ('unexpected line style'); + } + + printTree ($entry->{CHILDREF}, $lspecsref, $line_style, $max_report_width, $debug) if (exists $entry->{CHILDREF}); + } +} + +my $re_IP_strict = qr/\b(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\b/; +# XXX optimize this using packed default sorting. Analysis shows speed isn't an issue though +sub bycount { + # Sort by totals, then IP address if one exists, and finally by data as a string + + local $SIG{__WARN__} = sub { print "*** PLEASE REPORT:\n*** $_[0]*** Unexpected: \"$a->{DATA}\", \"$b->{DATA}\"\n" }; + + $b->{TOTAL} <=> $a->{TOTAL} + + || + + pack('C4' => $a->{DATA} =~ /^$re_IP_strict/o) cmp pack('C4' => $b->{DATA} =~ /^$re_IP_strict/o) + + || + + $a->{DATA} cmp $b->{DATA} +} + +# +# Builds a tree of REC structures from the multi-key %Counts hashes +# +# Parameters: +# Hash: A multi-key hash, with keys being used as category headings, and leaf data +# being tallies for that set of keys +# Level: This current recursion level. Call with 0. +# +# Returns: +# Listref: A listref, where each item in the list is a rec record, described as: +# DATA: a string: a heading, or log data +# TOTAL: an integer: which is the subtotal of this item's children +# LEVEL: an integer > 0: representing this entry's level in the tree +# CHILDREF: a listref: references a list consisting of this node's children +# Total: The cummulative total of items found for a given invocation +# +# Use the special key variable $END_KEY, which is "\a\a" (two ASCII bell's) to end a, +# nested hash early, or the empty string '' may be used as the last key. + +our $END_KEY = "\a\a"; + +sub buildTree(\% $ $ $ $ $) { + my ($href, $max_level_section, $levspecref, $max_level_global, $recurs_level, $show_unique, $debug) = @_; + my ($subtotal, $childList, $rec); + + my @treeList = (); + my $total = 0; + + foreach my $item (sort keys %$href) { + if (ref($href->{$item}) eq "HASH") { + #print " " x ($recurs_level * 4), "HASH: LEVEL $recurs_level: Item: $item, type: \"", ref($href->{$item}), "\"\n"; + + ($subtotal, $childList) = buildTree (%{$href->{$item}}, $max_level_section, $levspecref, $max_level_global, $recurs_level + 1, $debug); + + if ($recurs_level < $max_level_global and $recurs_level < $max_level_section) { + # me + children + $rec = { + DATA => $item, + TOTAL => $subtotal, + LEVEL => $recurs_level, + CHILDREF => $childList, + }; + + if ($debug) { + $rec->{DEBUG} = sprintf "L%d: levelspecs: %2d/%2d/%2d/%2d, Count: %10d", + $recurs_level + 1, $max_level_global, $max_level_section, + $levspecref->[$recurs_level]{topn}, $levspecref->[$recurs_level]{threshold}, $subtotal; + } + push (@treeList, $rec); + } + } + else { + if ($item ne '' and $item ne $END_KEY and $recurs_level < $max_level_global and $recurs_level < $max_level_section) { + $rec = { + DATA => $item, + TOTAL => $href->{$item}, + LEVEL => $recurs_level, + #CHILDREF => undef, + }; + if ($debug) { + $rec->{DEBUG} = sprintf "L%d: levelspecs: %2d/%2d/%2d/%2d, Count: %10d", + $recurs_level, $max_level_global, $max_level_section, + $levspecref->[$recurs_level]{topn}, $levspecref->[$recurs_level]{threshold}, $href->{$item}; + } + push (@treeList, $rec); + } + $subtotal = $href->{$item}; + } + + $total += $subtotal; + } + + #print " " x ($recurs_level * 4), "LEVEL $recurs_level: Returning from recurs_level $recurs_level\n"; + + return ($total, \@treeList); +} + +1; + +#MODULE: ../Logreporters/Reports.pm +package Logreporters::Reports; + +use 5.008; +use strict; +use re 'taint'; +use warnings; +no warnings "uninitialized"; + +BEGIN { + use Exporter (); + use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); + $VERSION = '1.002'; + @ISA = qw(Exporter); + @EXPORT = qw(&inc_unmatched &print_unmatched_report &print_percentiles_report2 + &print_summary_report &print_detail_report); + @EXPORT_OK = qw(); +} + +use subs @EXPORT_OK; + +BEGIN { + import Logreporters::Config qw(%Opts $fw1 $fw2 $sep1 $sep2 &D_UNMATCHED &D_TREE); + import Logreporters::Utils qw(&commify &unitize &get_percentiles &get_percentiles2); + import Logreporters::TreeData qw(%Totals %Counts &buildTree &printTree); +} + +my (%unmatched_list); + +our $origline; # unmodified log line, for error reporting and debug + +sub inc_unmatched($) { + my ($id) = @_; + $unmatched_list{$origline}++; + print "UNMATCHED($id): \"$origline\"\n" if $Opts{'debug'} & D_UNMATCHED; +} + +# Print unmatched lines +# +sub print_unmatched_report() { + return unless (keys %unmatched_list); + + print "\n\n**Unmatched Entries**\n"; + foreach my $line (sort {$unmatched_list{$b}<=>$unmatched_list{$a} } keys %unmatched_list) { + printf "%8d %s\n", $unmatched_list{$line}, $line; + } +} + +=pod + ****** Summary ******************************************************** + 2 Miscellaneous warnings + + 20621 Total messages scanned ---------------- 100.00% + 662.993M Total bytes scanned 695,198,092 + ======== ================================================ + + 19664 Ham ----------------------------------- 95.36% + 19630 Clean passed 95.19% + 34 Bad header passed 0.16% + + 942 Spam ---------------------------------- 4.57% + 514 Spam blocked 2.49% + 428 Spam discarded (no quarantine) 2.08% + + 15 Malware ------------------------------- 0.07% + 15 Malware blocked 0.07% + + + 1978 SpamAssassin bypassed + 18 Released from quarantine + 1982 Whitelisted + 3 Blacklisted + 12 MIME error + 51 Bad header (debug supplemental) + 28 Extra code modules loaded at runtime +=cut +# Prints the Summary report section +# +sub print_summary_report (\@) { + my ($sections) = @_; + my ($keyname,$cur_level); + my @lines; + + my $expand_header_footer = sub { + my $line = undef; + + foreach my $horf (@_) { + # print blank line if keyname is newline + if ($horf eq "\n") { + $line .= "\n"; + } + elsif (my ($sepchar) = ($horf =~ /^(.)$/o)) { + $line .= sprintf "%s %s\n", $sepchar x 8, $sepchar x 50; + } + else { + die "print_summary_report: unsupported header or footer type \"$horf\""; + } + } + return $line; + }; + + if ($Opts{'detail'} >= 5) { + my $header = "****** Summary "; + print $header, '*' x ($Opts{'max_report_width'} - length $header), "\n\n"; + } + + my @headers; + foreach my $sref (@$sections) { + # headers and separators + die "Unexpected Section $sref" if (ref($sref) ne 'HASH'); + + # Start of a new section group. + # Expand and save headers to output at end of section group. + if ($sref->{CLASS} eq 'GROUP_BEGIN') { + $cur_level = $sref->{LEVEL}; + $headers[$cur_level] = &$expand_header_footer(@{$sref->{HEADERS}}); + } + + elsif ($sref->{CLASS} eq 'GROUP_END') { + my $prev_level = $sref->{LEVEL}; + + # If this section had lines to output, tack on headers and footers, + # removing extraneous newlines. + if ($lines[$cur_level]) { + # squish multiple blank lines + if ($headers[$cur_level] and substr($headers[$cur_level],0,1) eq "\n") { + if ( ! defined $lines[$prev_level][-1] or $lines[$prev_level][-1] eq "\n") { + $headers[$cur_level] =~ s/^\n+//; + } + } + + push @{$lines[$prev_level]}, $headers[$cur_level] if $headers[$cur_level]; + push @{$lines[$prev_level]}, @{$lines[$cur_level]}; + my $f = &$expand_header_footer(@{$sref->{FOOTERS}}); + push @{$lines[$prev_level]}, $f if $f; + $lines[$cur_level] = undef; + } + + $headers[$cur_level] = undef; + $cur_level = $prev_level; + } + + elsif ($sref->{CLASS} eq 'DATA') { + # Totals data + $keyname = $sref->{NAME}; + if ($Totals{$keyname} > 0) { + my ($numfmt, $desc, $divisor) = ($sref->{FMT}, $sref->{TITLE}, $sref->{DIVISOR}); + + my $fmt = '%8'; + my $extra = ' %25s'; + my $total = $Totals{$keyname}; + + # Z format provides unitized or unaltered totals, as appropriate + if ($numfmt eq 'Z') { + ($total, $fmt) = unitize ($total, $fmt); + } + else { + $fmt .= "$numfmt "; + $extra = ''; + } + + if ($divisor and $$divisor) { + # XXX generalize this + if (ref ($desc) eq 'ARRAY') { + $desc = @$desc[0] . ' ' . @$desc[1] x (42 - 2 - length(@$desc[0])); + } + + push @{$lines[$cur_level]}, + sprintf "$fmt %-42s %6.2f%%\n", $total, $desc, + $$divisor == $Totals{$keyname} ? 100.00 : $Totals{$keyname} * 100 / $$divisor; + } + else { + push @{$lines[$cur_level]}, + sprintf "$fmt %-23s $extra\n", $total, $desc, commify ($Totals{$keyname}); + } + } + } + else { + die "print_summary_report: unexpected control..."; + } + } + print @{$lines[0]}; + print "\n"; +} + +# Prints the Detail report section +# +# Note: side affect; deletes each key in Totals/Counts +# after printout. Only the first instance of a key in +# the Section table will result in Detail output. +sub print_detail_report (\@) { + my ($sections) = @_; + my $header_printed = 0; + + return unless (keys %Counts); + +#use Devel::Size qw(size total_size); + + foreach my $sref ( @$sections ) { + next unless $sref->{CLASS} eq 'DATA'; + # only print detail for this section if DETAIL is enabled + # and there is something in $Counts{$keyname} + next unless $sref->{DETAIL}; + next unless exists $Counts{$sref->{NAME}}; + + my $keyname = $sref->{NAME}; + my $max_level = undef; + my $print_this_key = 0; + + my @levelspecs = (); + clear_level_specs($max_level, \@levelspecs); + if (exists $Opts{$keyname}) { + $max_level = create_level_specs($Opts{$keyname}, $Opts{'detail'}, \@levelspecs); + $print_this_key = 1 if ($max_level); + } + else { + $print_this_key = 1; + } + #print_level_specs($max_level,\@levelspecs); + + # at detail 5, print level 1, detail 6: level 2, ... + +#print STDERR "building: $keyname\n"; + my ($count, $treeref) = + buildTree (%{$Counts{$keyname}}, defined ($max_level) ? $max_level : 11, + \@levelspecs, $Opts{'detail'} - 4, 0, $Opts{'debug'} & D_TREE); + + if ($count > 0) { + if ($print_this_key) { + my $desc = $sref->{TITLE}; + $desc =~ s/^\s+//; + + if (! $header_printed) { + my $header = "****** Detail ($max_level) "; + print $header, '*' x ($Opts{'max_report_width'} - length $header), "\n"; + $header_printed = 1; + } + printf "\n%8d %s %s\n", $count, $desc, + $Opts{'sect_vars'} ? + ('-' x ($Opts{'max_report_width'} - 18 - length($desc) - length($keyname))) . " [ $keyname ] -" : + '-' x ($Opts{'max_report_width'} - 12 - length($desc)) + } + + printTree ($treeref, \@levelspecs, $Opts{'line_style'}, $Opts{'max_report_width'}, + $Opts{'debug'} & D_TREE); + } +#print STDERR "Total size Counts: ", total_size(\%Counts), "\n"; +#print STDERR "Total size Totals: ", total_size(\%Totals), "\n"; + $treeref = undef; + $Totals{$keyname} = undef; + delete $Totals{$keyname}; + delete $Counts{$keyname}; + } + #print "\n"; +} + +=pod + +Print out a standard percentiles report + + === Delivery Delays Percentiles =============================================================== + 0% 25% 50% 75% 90% 95% 98% 100% + ----------------------------------------------------------------------------------------------- + Before qmgr 0.01 0.70 1.40 45483.70 72773.08 81869.54 87327.42 90966.00 + In qmgr 0.00 0.00 0.00 0.01 0.01 0.01 0.01 0.01 + Conn setup 0.00 0.00 0.00 0.85 1.36 1.53 1.63 1.70 + Transmission 0.03 0.47 0.92 1.61 2.02 2.16 2.24 2.30 + Total 0.05 1.18 2.30 45486.15 72776.46 81873.23 87331.29 90970.00 + =============================================================================================== + + === Postgrey Delays Percentiles =========================================================== + 0% 25% 50% 75% 90% 95% 98% 100% + ------------------------------------------------------------------------------------------- + Postgrey 727.00 727.00 727.00 727.00 727.00 727.00 727.00 727.00 + =========================================================================================== + + tableref: + data table: ref to array of arrays, first cell is label, subsequent cells are data + title: + table's title + percentiles_str: + string of space or comma separated integers, which are the percentiles + calculated and output as table column data +=cut +sub print_percentiles_report2($$$) { + my ($tableref, $title, $percentiles_str) = @_; + + return unless @$tableref; + + my $myfw2 = $fw2 - 1; + my @percents = split /[ ,]/, $percentiles_str; + + # Calc y label width from the hash's keys. Each key is padded with the + # string "#: ", # where # is a single-digit sort index. + my $y_label_max_width = 0; + for (@$tableref) { + $y_label_max_width = length($_->[0]) if (length($_->[0]) > $y_label_max_width); + } + + # Titles row + my $col_titles_str = sprintf "%-${y_label_max_width}s" . "%${myfw2}s%%" x @percents , ' ', @percents; + my $table_width = length($col_titles_str); + + # Table header row + my $table_header_str = sprintf "%s %s ", $sep1 x 3, $title; + $table_header_str .= $sep1 x ($table_width - length($table_header_str)); + + print "\n", $table_header_str; + print "\n", $col_titles_str; + print "\n", $sep2 x $table_width; + + my (@p, @coldata, @xformed); + foreach (@$tableref) { + my ($title, $ref) = ($_->[0], $_->[1]); + #xxx my @sorted = sort { $a <=> $b } @{$_->[1]}; + + my @byscore = (); + + for my $bucket (sort { $a <=> $b } keys %$ref) { + #print "Key: $title: Bucket: $bucket = $ref->{$bucket}\n"; + # pairs: bucket (i.e. key), tally + push @byscore, $bucket, $ref->{$bucket}; + } + + + my @p = get_percentiles2 (@byscore, @percents); + printf "\n%-${y_label_max_width}s" . "%${fw2}.2f" x scalar (@p), $title, @p; + } + +=pod + foreach (@percents) { + #printf "\n%-${y_label_max_width}s" . "%${fw2}.2f" x scalar (@p), substr($title,3), @p; + printf "\n%3d%%", $title; + foreach my $val (@{shift @xformed}) { + my $unit; + if ($val > 1000) { + $unit = 's'; + $val /= 1000; + } + else { + $unit = ''; + } + printf "%${fw3}.2f%-2s", $val, $unit; + } + } +=cut + + print "\n", $sep1 x $table_width, "\n"; +} + +sub clear_level_specs($ $) { + my ($max_level,$lspecsref) = @_; + #print "Zeroing $max_level rows of levelspecs\n"; + $max_level = 0 if (not defined $max_level); + for my $x (0..$max_level) { + $lspecsref->[$x]{topn} = undef; + $lspecsref->[$x]{threshold} = undef; + } +} + +# topn = 0 means don't limit +# threshold = 0 means no min threshold +sub create_level_specs($ $ $) { + my ($optkey,$gdetail,$lspecref) = @_; + + return 0 if ($optkey eq "0"); + + my $max_level = $gdetail; # default to global detail level + my (@specsP1, @specsP2, @specsP3); + + #printf "create_level_specs: key: %s => \"%s\", max_level: %d\n", $optkey, $max_level; + + foreach my $sp (split /[\s,]+/, $optkey) { + #print "create_level_specs: SP: \"$sp\"\n"; + # original level specifier + if ($sp =~ /^\d+$/) { + $max_level = $sp; + #print "create_level_specs: max_level set: $max_level\n"; + } + # original level specifier + topn at level 1 + elsif ($sp =~ /^(\d*)\.(\d+)$/) { + if ($1) { $max_level = $1; } + else { $max_level = $gdetail; } # top n specified, but no max level + + # force top N at level 1 (zero based) + push @specsP1, { level => 0, topn => $2, threshold => 0 }; + } + # newer level specs + elsif ($sp =~ /^::(\d+)$/) { + push @specsP3, { level => undef, topn => 0, threshold => $1 }; + } + elsif ($sp =~ /^:(\d+):(\d+)?$/) { + push @specsP2, { level => undef, topn => $1, threshold => defined $2 ? $2 : 0 }; + } + elsif ($sp =~ /^(\d+):(\d+)?:(\d+)?$/) { + push @specsP1, { level => ($1 > 0 ? $1 - 1 : 0), topn => $2 ? $2 : 0, threshold => $3 ? $3 : 0 }; + } + else { + print STDERR "create_level_specs: unexpected levelspec ignored: \"$sp\"\n"; + } + } + + #foreach my $sp (@specsP3, @specsP2, @specsP1) { + # printf "Sorted specs: L%d, topn: %3d, threshold: %3d\n", $sp->{level}, $sp->{topn}, $sp->{threshold}; + #} + + my ($min, $max); + foreach my $sp ( @specsP3, @specsP2, @specsP1) { + ($min, $max) = (0, $max_level); + + if (defined $sp->{level}) { + $min = $max = $sp->{level}; + } + for my $level ($min..$max) { + #printf "create_level_specs: setting L%d, topn: %s, threshold: %s\n", $level, $sp->{topn}, $sp->{threshold}; + $lspecref->[$level]{topn} = $sp->{topn} if ($sp->{topn}); + $lspecref->[$level]{threshold} = $sp->{threshold} if ($sp->{threshold}); + } + } + + return $max_level; +} + +sub print_level_specs($ $) { + my ($max_level,$lspecref) = @_; + for my $level (0..$max_level) { + printf "LevelSpec Row %d: %3d %3d\n", $level, $lspecref->[$level]{topn}, $lspecref->[$level]{threshold}; + } +} + + +1; + + +package Logreporters; + +BEGIN { + import Logreporters::Utils; + import Logreporters::Config; + import Logreporters::TreeData qw(%Totals %Counts %Collecting printTree buildTree); + import Logreporters::Reports; +} +use 5.008; +use strict; +use warnings; +no warnings "uninitialized"; +use re 'taint'; + +use Getopt::Long; +use File::Basename; + +our $progname = fileparse($0); + +# the list of supplemental reports available in the Detail section +#p0f +my @supplemental_reports = qw( + autolearn score_percentiles score_frequencies sarules timings sa_timings startinfo +); + +# Default values for various options, used if no config file exists, +# or some option is not set. +# +# These are used to reset default values after an option has been +# disabled (via undef'ing its value). This allows a report to be +# disabled via config file or --nodetail, but reenabled via subsequent +# command line option +my %Defaults = ( + detail => 10, # report level detail + max_report_width => 100, # maximum line width for report output + line_style => undef, # lines > max_report_width, 0=truncate,1=wrap,2=full + syslog_name => $progname_prefix, # amavis' syslog service name + sect_vars => 0, # show section vars in detail report hdrs + ipaddr_width => 15, # width for printing ip addresses + first_recip_only => 0, # Show only the first recipient, or all + + autolearn => 1, # show Autolearn report + bayes => 1, # show hit Bayesian buckets + #p0f => 'all all', # p0f hits report + sarules => '20 20', # show SpamAssassin rules hit + score_frequencies => '-10 -5 0 5 10 20 30', # buckets shown in spam scores report + score_percentiles => '0 50 90 95 98 100', # percentiles shown in spam scores report + startinfo => 1, # show amavis startup info + timings => 95, # show top N% of the timings report + timings_percentiles => '0 5 25 50 75 95 100', # percentiles shown in timing report + sa_timings => 95, # show top N% of the SA timings report + sa_timings_percentiles => '0 5 25 50 75 95 100', # percentiles shown in SA timing report +); + +my $usage_str = <<"END_USAGE"; +Usage: $progname [ ARGUMENTS ] [logfile ...] + + ARGUMENTS can be one or more of options listed below. Later options override earlier ones. + Any argument may be abbreviated to an unambiguous length. Input comes from named logfiles, + or STDIN. + + --debug AREAS provide debug output for AREAS + --help print usage information + --version print program version + + --config_file FILE, -f FILE use alternate configuration file FILE + --syslog_name PATTERN only consider log lines that match + syslog service name PATTERN + + --detail LEVEL print LEVEL levels of detail + (default: 10) + --nodetail set all detail levels to 0 + --[no]summary display the summary section + + --ipaddr_width WIDTH use WIDTH chars for IP addresses in + address/hostname pairs + --line_style wrap|full|truncate disposition of lines > max_report_width + (default: truncate) + --full same as --line_style=full + --truncate same as --line_style=truncate + --wrap same as --line_style=wrap + --max_report_width WIDTH limit report width to WIDTH chars + (default: 100) + --limit L=V, -l L=V set level limiter L with value V + --[no]sect_vars [do not] show config file var/cmd line + option names in section titles + + --[no]autolearn show autolearn report + --[no]by_ccat_summary include by contents category grouping in summary + --[no]first_recip_only show first recipient only, or all recipients + --nosarules disable SpamAssassin spam and ham rules hit reports + --sarules "S,H" enable SpamAssassin spam and ham rules reports, showing + --sarules "default" showing the top S spam and top H ham rules hit (range: + 0..., "all", or the keyword "default"). + --noscore_frequencies disable spam score frequency report + --score_frequencies "B1 [B2 ...]" enable spam score frequency report, using buckets + --score_frequencies "default" specified with B1 [B2 ...] (range: real numbers), or using their + internal default values when the keyword "default" is given + --noscore_percentiles disable spam score percentiles report + --score_percentiles "P1 [P2 ...]" enable spam score percentiles report, using percentiles + --score_percentiles "default" specified with P1 [P2 ...] (range: 0...100), or using their + internal default values when the keyword "default" is given + --[no]startinfo show latest amavis startup details, if available + + --nosa_timings disable the SA timings report (same as --sa_timings 0) + --sa_timings PERCENT show top PERCENT percent of the SA timings report (range: 0...100) + --sa_timings_percentiles "P1 [P2 ...]" + set SA timings report percentiles to P1 [P2 ...] (range: 0...100) + + --notimings disable the timings report (same as --timings 0) + --timings PERCENT show top PERCENT percent of the timings report (range: 0...100) + --timings_percentiles "P1 [P2 ...]" set timings report percentiles to P1 [P2 ...] (range: 0...100) +END_USAGE + +# local prototypes +sub usage($); +sub init_getopts_table(); +sub init_defaults(); +sub build_sect_table(); + +sub parse_vals($$); +sub triway_opts($$); + +sub printSpamScorePercentilesReport; +sub printSpamScoreFrequencyReport; +sub printAutolearnReport; +sub printSARulesReport; +sub printTimingsReport($$$$); +sub printStartupInfoReport; +sub strip_trace($); +sub prioritize_cmdline(@); + +sub create_ignore_list(); +sub check_ignore_list($ \@); + +# lines that match any RE in this list will be ignored. +# see create_ignore_list(); +my @ignore_list_final = (); + +# The Sections table drives Summary and Detail reports. For each entry in the +# table, if there is data avaialable, a line will be output in the Summary report. +# Additionally, a sub-section will be output in the Detail report if both the +# global --detail, and the section's limiter variable, are sufficiently high (a +# non-existent section limiter variable is considered to be sufficiently high). +# +my @Sections; + +# Initialize main running mode and basic opts +init_run_mode($config_file); + +# Configure the Getopts options table +init_getopts_table(); + +# Place configuration file/environment variables onto command line +init_cmdline(); + +# Initialize default values +init_defaults(); + +# Process command line arguments, 0=no_permute,no_pass_through +get_options(0); + +# Build the Section table +build_sect_table(); + +# Run through the list of Limiters, setting the limiters in %Opts. +process_limiters(@Sections); + +# Set collection for any enabled supplemental sections +foreach (@supplemental_reports) { + $Logreporters::TreeData::Collecting{$_} = (($Opts{'detail'} >= 5) && $Opts{$_}) ? 1 : 0; +} + +# Don't collect SpamScores when not necessary +$Collecting{'spamscores'} = ($Opts{'detail'} >= 5 && ($Opts{'score_percentiles'} || $Opts{'score_frequencies'})) ? 1 : 0; + +if (! defined $Opts{'line_style'}) { + # default line style to full if detail >= 11, or truncate otherwise + $Opts{'line_style'} = + ($Opts{'detail'} > 10) ? $line_styles{'full'} : $line_styles{'truncate'}; +} + +# Create the list of REs used to match against log lines +create_ignore_list(); + +my (%Timings, %TimingsSA, @TimingsTotals, @TimingsSATotals); +my (%SaveLine, %StartInfo); +my (%SpamScores, %spamtags, %p0ftags); + +# Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN +my %ccatmajor_to_sectkey = ( + 'INFECTED' => 'malware', + 'BANNED' => 'bannedname', + 'UNCHECKED' => 'unchecked', + 'SPAM' => 'spam', + 'SPAMMY' => 'spammy', + 'BAD-HEADER' => 'badheader', + 'OVERSIZED' => 'oversized', + 'MTA-BLOCKED' => 'mta', + 'CLEAN' => 'clean', + 'TEMPFAIL' => 'tempfail', + 'OTHER' => 'other', +); + +my %ccatmajor_to_priority = ( + 'INFECTED' => 9, + 'BANNED' => 8, + 'UNCHECKED' => 7, + 'SPAM' => 6, + 'SPAMMY' => 5, + 'BAD-HEADER' => 4, + 'OVERSIZED' => 3, + 'MTA-BLOCKED' => 2, + 'CLEAN' => 1, + 'TEMPFAIL' => 0, + 'OTHER' => 0, +); + +# for reports +my %ccatmajor_to_spamham = ( + 'INFECTED' => 'malware', + 'BANNED' => 'bannedname', + 'UNCHECKED' => 'unchecked', + 'SPAM' => 'spam', + 'SPAMMY' => 'spam', + 'BAD-HEADER' => 'ham', + 'OVERSIZED' => 'ham', + 'MTA-BLOCKED' => 'ham', + 'CLEAN' => 'ham', + 'TEMPFAIL' => 'ham', + 'OTHER' => 'ham', +); + +my $logline_maxlen = 980; + +# Create the list of REs against which log lines are matched. +# Lines that match any of the patterns in this list are ignored. +# +# Note: This table is created at runtime, due to a Perl bug which +# I reported as perl bug #56202: +# +# http://rt.perl.org/rt3/Public/Bug/Display.html?id=56202 +# + +sub create_ignore_list() { + push @ignore_list_final, qr/^lookup_ip_acl/; + push @ignore_list_final, qr/^lookup_acl/; + push @ignore_list_final, qr/^lookup_hash/; + push @ignore_list_final, qr/^lookup_re/; + push @ignore_list_final, qr/^lookup_ldap/; + push @ignore_list_final, qr/^lookup_sql_field.* result=[YN]$/; + push @ignore_list_final, qr/^lookup .* does not match$/; + push @ignore_list_final, qr/^lookup [[(]/; + push @ignore_list_final, qr/^lookup => /; + push @ignore_list_final, qr/^lookup: /; + push @ignore_list_final, qr/^save_info_preliminary/; # log level 4 + push @ignore_list_final, qr/^save_info_final/; # log level 4 + push @ignore_list_final, qr/^sql: /; + push @ignore_list_final, qr/^sql_storage: retrying/; + push @ignore_list_final, qr/^sql flush: /; + push @ignore_list_final, qr/^sql print/; + push @ignore_list_final, qr/^sql begin transaction/; + push @ignore_list_final, qr/^sql rollback/; + push @ignore_list_final, qr/^mail_via_sql: /; + push @ignore_list_final, qr/^CALLING SA check$/; + push @ignore_list_final, qr/^calling SA parse,/; + push @ignore_list_final, qr/^timer set to \d+/; + push @ignore_list_final, qr/^query_keys/; + push @ignore_list_final, qr/^find_or_save_addr: /; + push @ignore_list_final, qr/^header: /; + push @ignore_list_final, qr/^DO_QUARANTINE, /; + push @ignore_list_final, qr/^DEBUG_ONESHOT: /; + push @ignore_list_final, qr/^TempDir::/; + push @ignore_list_final, qr/^check_mail_begin_task: /; + push @ignore_list_final, qr/^program: .*?(anomy|altermime|disclaimer).*? said: /; # log_level 2 + push @ignore_list_final, qr/^body (?:type|hash): /; + push @ignore_list_final, qr/^\d+\.From: <.*>, \d+.Mail_From:/; + push @ignore_list_final, qr/^The amavisd daemon is (?:apparently )?not running/; + push @ignore_list_final, qr/^rw_loop/; + push @ignore_list_final, qr/^[SL]MTP[><]/; + push @ignore_list_final, qr/^[SL]MTP response for/; + push @ignore_list_final, qr/^dsn:/i, # DSN or dsn + push @ignore_list_final, qr/^enqueue: /; + push @ignore_list_final, qr/^write_header: /; + push @ignore_list_final, qr/^banned check: /; + push @ignore_list_final, qr/^child_finish_hook/; + push @ignore_list_final, qr/^inspect_dsn:/; + push @ignore_list_final, qr/^client IP address unknown/; + push @ignore_list_final, qr/^final_destiny/; + push @ignore_list_final, qr/^one_response_for_all/; + push @ignore_list_final, qr/^headers CLUSTERING/; + push @ignore_list_final, qr/^notif=/; + push @ignore_list_final, qr/^\(about to connect/; + push @ignore_list_final, qr/^Original mail size/; + push @ignore_list_final, qr/^TempDir removal/; + push @ignore_list_final, qr/^Issued a new file name/; + push @ignore_list_final, qr/^starting banned checks/; + push @ignore_list_final, qr/^skip admin notification/; + push @ignore_list_final, qr/^do_notify_and_quarantine - done/; + push @ignore_list_final, qr/^do_[a-zA-Z]+.* done$/i; + push @ignore_list_final, qr/^Remote host presents itself as:/; + push @ignore_list_final, qr/^connect_to_ldap/; + push @ignore_list_final, qr/^connect_to_sql: trying /; + push @ignore_list_final, qr/^ldap begin_work/; + push @ignore_list_final, qr/^Connecting to LDAP server/; + push @ignore_list_final, qr/^loaded base policy bank/; + push @ignore_list_final, qr/^\d+\.From:/; + push @ignore_list_final, qr/^Syslog (retries|warnings)/; + push @ignore_list_final, qr/^smtp connection cache/; + push @ignore_list_final, qr/^smtp cmd> /; + push @ignore_list_final, qr/^smtp session/; + push @ignore_list_final, qr/^Ignoring stale PID file/; + push @ignore_list_final, qr/^mime_decode_preamble/; + push @ignore_list_final, qr/^doing banned check for/; + push @ignore_list_final, qr/^open_on_specific_fd/; + push @ignore_list_final, qr/^reparenting /; + push @ignore_list_final, qr/^Issued a new pseudo part: /; + push @ignore_list_final, qr/^run_command: /; + push @ignore_list_final, qr/^result line from file/; + push @ignore_list_final, qr/^Charging /; + push @ignore_list_final, qr/^check_for_banned /; + push @ignore_list_final, qr/^Extracting mime components$/; + push @ignore_list_final, qr/^response to /; + push @ignore_list_final, qr/^File-type of /; + push @ignore_list_final, qr/^Skip admin notification, /; + push @ignore_list_final, qr/^run_av: /; + push @ignore_list_final, qr/^string_to_mime_entity /; + push @ignore_list_final, qr/^ndn_needed=/; + push @ignore_list_final, qr/^sending RCPT TO:/; + push @ignore_list_final, qr/^decode_parts: /; + push @ignore_list_final, qr/^decompose_part: /; + push @ignore_list_final, qr/^setting body type: /; + push @ignore_list_final, qr/^mime_decode_epilogue: /; + push @ignore_list_final, qr/^string_to_mime_entity: /; + push @ignore_list_final, qr/^at the END handler: /; + push @ignore_list_final, qr/^Amavis::.* called$/; + push @ignore_list_final, qr/^Amavis::.* close,/; + push @ignore_list_final, qr/^dkim: /; # XXX provide stats + push @ignore_list_final, qr/^collect banned table/; + push @ignore_list_final, qr/^collect_results from/; + push @ignore_list_final, qr/^blocking contents category is/; + push @ignore_list_final, qr/^running file\(/; + push @ignore_list_final, qr/^Found av scanner/; + push @ignore_list_final, qr/^Found myself/; + push @ignore_list_final, qr/^mail_via_smtp/; + push @ignore_list_final, qr/^switch_to_client_time/; + push @ignore_list_final, qr/^parse_message_id/; + push @ignore_list_final, qr/^parse_received: /; + push @ignore_list_final, qr/^parse_ip_address_from_received: /; + push @ignore_list_final, qr/^fish_out_ip_from_received: /; + push @ignore_list_final, qr/^Waiting for the process \S+ to terminate/; + push @ignore_list_final, qr/^Valid PID file \(younger than sys uptime/; + push @ignore_list_final, qr/^Sending SIG\S+ to amavisd/; + push @ignore_list_final, qr/^Can't send SIG\S+ to process/; + push @ignore_list_final, qr/^killing process/; + push @ignore_list_final, qr/^no need to kill process/; + push @ignore_list_final, qr/^process .* is still alive/; + push @ignore_list_final, qr/^Daemon \[\d+\] terminated by SIG/; + push @ignore_list_final, qr/^storage and lookups will use .* to SQL/; + push @ignore_list_final, qr/^idle_proc, /; + push @ignore_list_final, qr/^switch_to_my_time/; + push @ignore_list_final, qr/^TempDir::strip: /; + push @ignore_list_final, qr/^rmdir_recursively/; + push @ignore_list_final, qr/^sending [SL]MTP response/; + push @ignore_list_final, qr/^prolong_timer/; + push @ignore_list_final, qr/^process_request:/; + push @ignore_list_final, qr/^exiting process_request/; + push @ignore_list_final, qr/^post_process_request_hook: /; + push @ignore_list_final, qr/^SMTP session over/; + push @ignore_list_final, qr/^updating snmp variables/; + push @ignore_list_final, qr/^best_try_originator_ip/; + push @ignore_list_final, qr/^mail checking ended: /; # log level 2 + push @ignore_list_final, qr/^The amavisd daemon is already running/; + push @ignore_list_final, qr/^AUTH not needed/; + push @ignore_list_final, qr/^load: \d+ %, total idle/; + push @ignore_list_final, qr/^policy protocol: [^=]+=\S+(?:,\S+)*$/; # allow "policy protocol: INVALID ..." later + push @ignore_list_final, qr/^penpals: /; + push @ignore_list_final, qr/^Not calling virus scanners, no files to scan in/; + push @ignore_list_final, qr/^local delivery: /; + push @ignore_list_final, qr/^run_as_subprocess: child process \S*: Broken pipe/; + push @ignore_list_final, qr/^initializing Mail::SpamAssassin/; + push @ignore_list_final, qr/^Error reading mail header section/; # seems to occur gen. due to perl getline() bug + push @ignore_list_final, qr/^flatten_and_tidy_dir/; + push @ignore_list_final, qr/^do_7zip: member/; + push @ignore_list_final, qr/^Expanding \S+ archive/; + push @ignore_list_final, qr/^files_to_scan:/; + push @ignore_list_final, qr/^Unzipping p\d+/; + push @ignore_list_final, qr/^writing mail text to SQL/; + push @ignore_list_final, qr/^strip_tempdir/; + push @ignore_list_final, qr/^no parts, file/; + push @ignore_list_final, qr/^warnsender_with_pass/; + push @ignore_list_final, qr/^RETURNED FROM SA check/; + push @ignore_list_final, qr/^mime_traverse: /; + push @ignore_list_final, qr/^do_spam: /; + push @ignore_list_final, qr/^prepare_tempdir: /; + push @ignore_list_final, qr/^check_header: /; + push @ignore_list_final, qr/^skip admin notification/; + push @ignore_list_final, qr/^do_executable: not a/; + push @ignore_list_final, qr/^Skip spam admin notification, no administrators$/; + push @ignore_list_final, qr/^skip banned check for/; + push @ignore_list_final, qr/^is_outgoing /; + push @ignore_list_final, qr/^NO Disclaimer/; + push @ignore_list_final, qr/^Using \(\S+\) on file/; + push @ignore_list_final, qr/^no anti-spam code loaded/; + push @ignore_list_final, qr/^entered child_init_hook/; + push @ignore_list_final, qr/^body type/; + push @ignore_list_final, qr/^establish_or_refresh/; + push @ignore_list_final, qr/^get_body_digest/; + push @ignore_list_final, qr/^ask_daemon_internal/; + push @ignore_list_final, qr/^Turning AV infection into a spam report, name already accounted for/; + push @ignore_list_final, qr/^Calling virus scanners/; + push @ignore_list_final, qr/^timer stopped after /; + push @ignore_list_final, qr/^virus_presence /; + push @ignore_list_final, qr/^cache entry /; + push @ignore_list_final, qr/^generate_mail_id /; + push @ignore_list_final, qr/^Load low precedence policybank/; + push @ignore_list_final, qr/^warm restart on /; # XXX could be placed instartup info + push @ignore_list_final, qr/^Signalling a SIGHUP to a running daemon/; + push @ignore_list_final, qr/^Deleting db files /; + push @ignore_list_final, qr/^address modified \(/; + push @ignore_list_final, qr/^Request: AM\.PDP /; + push @ignore_list_final, qr/^DSPAM result: /; + push @ignore_list_final, qr/^bind to \//; + push @ignore_list_final, qr/^ZMQ enabled: /; + + push @ignore_list_final, qr/^Inserting header field: X-Amavis-Hold: /; + push @ignore_list_final, qr/^Decoding of .* failed, leaving it unpacked: /; + + # various forms of "Using ..." + # more specific, interesting variants already captured: search "Using" + push @ignore_list_final, qr/^Using \(.*\) on dir:/; + push @ignore_list_final, qr/^Using [^:]+: \(built-in interface\)/; + push @ignore_list_final, qr/^Using \(.*\): /; + push @ignore_list_final, qr/: sleeping for /; + push @ignore_list_final, qr/creating socket by /; + + # unanchored + push @ignore_list_final, qr/\bRUSAGE\b/; + push @ignore_list_final, qr/: Sending .* to UNIX socket/; +} + +# Notes: +# +# - IN REs, always use /o flag or qr// at end of RE when RE uses unchanging interpolated vars +# - In REs, email addresses may be empty "<>" - capture using *, not + ( eg. from=<[^>]*> ) +# - See additional notes below, search for "Note:". +# - XXX indicates change, fix or more thought required + +# Main processing loop +# +while (<>) { + chomp; + s/ +$//; + next if $_ eq ''; + + $Logreporters::Reports::origline = $_; + + if ($Opts{'standalone'}) { + next unless s/^[A-Z][a-z]{2} [ \d]\d \d{2}:\d{2}:\d{2} (?:<[^>]+> )?\S+ $Opts{'syslog_name'}(?:\[\d+\])?: (?:\[ID \d+ \w+\.\w+\] )?//o; + } + + my $p1 = $_; + my ($p2, $pid); + my $action = "blocked"; # default action is blocked if not present in log + + # For now, ignore the amavis startup timing lines. Need to do this + # before stripping out the amavis pid to differentiate these from the + # scan timing reports + next if ($p1 =~ /^TIMING/); + + my $linelen = length $p1; + # Strip amavis process id-instance id, or release id + if (($pid,$p2) = ($p1 =~ /^\(([^)]+)\) (.*)$/ )) { + $p1 = $p2; + } + + # Handle continuation lines. Continuation lines should be in order per PID, meaning line1, line2, line3, + # but never line3, line1, line2. + # + # amavis log lines as chopped by sub write_log are exactly 980 characters long starting with '(' as in: + # amavis[47061]: (47061-15) SPAM, etc ... + # ^ <-----980-------------> + # but this can be changed in amavis via $logline_maxlen. + # There may also be the alert markers (!) and (!!) preceeding any continuation ellipsis. + # + + # ... a continued line ... + if ($p1 =~ s/^(\([!]{1,2}\))?\.\.\.//) { + if (!exists($SaveLine{$pid})) { + my $alert = $1; + #printf "Unexpected continue line: \"%s\"\n", $p1; + $SaveLine{$pid} = $alert || ''; + } + $SaveLine{$pid} .= $p1; + next if $SaveLine{$pid} =~ s/\.\.\.$//; # next if line has more pieces + } + + # this line continues ... + if ($p1 =~ /\.\.\.$/ and $linelen == $logline_maxlen) { + $p1 =~ s/\.\.\.$//; + $SaveLine{$pid} = $p1; + next; + } + + if (exists($SaveLine{$pid})) { + # printf "END OF SaveLine: %s\n", $SaveLine{$pid}; + $p1 = delete $SaveLine{$pid}; + } + + #if (length($p1) > 10000) { + # printf "Long log entry %d chars: \"%s\"\n", length($p1), $p1; + # next; + #} + + next if ( + # Place REs here that should ignore log lines otherwise caught below. + # Some are located here historically, and need to be checked for candidates + # to be relocated to ignore_list_final. + ($p1 =~ /^do_ascii/) + or ($p1 =~ /^Checking/) + or ($p1 =~ /^header_edits_for_quar: /) + or ($p1 =~ /^Not-Delivered/) + or ($p1 =~ /^SpamControl/) + or ($p1 =~ /^Perl/) + or ($p1 =~ /^ESMTP/) + or ($p1 =~ /^(?:\(!+\))?(\S+ )?(?:FWD|SEND) from /) # log level 4 + or ($p1 =~ /^(?:\(!+\))?(\S+ )?(?:ESMTP|FWD|SEND) via /) # log level 4 + or ($p1 =~ /^tempdir being removed/) + or ($p1 =~ /^do_notify_and_quar(?:antine)?: .*ccat/) + or ($p1 =~ /^cached [a-zA-Z0-9]+ /) + or ($p1 =~ /^loaded policy bank/) + or ($p1 =~ /^p\.path/) + or ($p1 =~ /^virus_scan: /) + or ($p1 =~ /^Requesting (a |)process rundown after [0-9]+ tasks/) + or ($p1 =~ /^Cached (virus|spam) check expired/) + or ($p1 =~ /^pr(?:esent|ovid)ing full original message to scanners as/) # log level 2 + or ($p1 =~ /^Actual message size [0-9]+ B(,| greater than the) declared [0-9]+ B/) + or ($p1 =~ /^disabling DSN/) + or ($p1 =~ /^Virus ([^,]+ )?matches [^,]+, sender addr ignored/) + or ($p1 =~ /^release /) + or ($p1 =~ /^adding SA score \S+ to existing/) + or ($p1 =~ /^Maia:/) # redundant + or ($p1 =~ /^AM\.PDP /) # this appears to be always have two spaces + # because in amavisd::preprocess_policy_query() when $ampdp is + # set, it will pass an unset $attr_ref->{'mail_id'} to do_log(1 + or ($p1 =~ /^_(?:WARN|DIE):$/) # bug: empty _WARN|_DIE: http://marc.info/?l=amavis-user&m=121725098111422&w=2 + + # non-begin anchored + or ($p1 =~ /result: clean$/) + or ($p1 =~ /DESTROY called$/) + or ($p1 =~ /email\.txt no longer exists, can't re-use it/) + or ($p1 =~ /SPAM\.TAG2/) + or ($p1 =~ /BAD-HEADER\.TAG2/) + or ($p1 =~ /: Connecting to socket/) + or ($p1 =~ /broken pipe \(don't worry\), retrying/) + or ($p1 =~ /(?:Sending|on dir:) (?:CONT)?SCAN /) + ); + + my ($ip, $from, $to, $key,, $reason, $item, + $decoder, $scanner, $stage, $sectkey); + + # Coerce older "INFECTED" quarantined lines into "Blocked INFECTED", + # to be processed in the Passed/Blocked section. + if ($p1 =~ /^INFECTED.*, quarantine/) { + $p1 = 'Blocked ' . $p1; + } + + # SPAM entry occurs at kill level + # SPAM-TAG entry occurs at log level 2, when spam header is inserted + # log_level >= 2 || (log_level > 2 && syslog_priority=debug) + my ($tagtype,$fromto,$isspam,$tags,$tests,$autolearn); + + # amavisd-new 2.7.0 changes SPAM-TAG to Spam-tag and its log_level to 3 + if (($tagtype,$fromto,$isspam,$tags,$tests,$autolearn) = ($p1 =~ /^((?i:SPAM(?:-TAG)?)), (.*), (Yes|No), score=[-+x\d.]+(.*) tests=\[([^\]]*)](?:, autolearn=(\w+))?/) or + ($tagtype,$fromto,$isspam,$tags,$tests) = ($p1 =~ /^((?i:SPAM(?:-TAG)?)), (.*), (Yes|No), hits=[-+x\d.]+(.*) tests=(.*)(?:, quarantine )?/)) { + + #TD SPAM, -> , Yes, score=17.709 tag=-10 tag2=6.31 kill=6.31 tests=[AWL=-0.678, BAYES_99=4], autolearn=spam, quarantine Cc4+GUJhgpqh (spam-quarantine) + #TD SPAM, -> , Yes, score=21.161 tag=x tag2=8.15 kill=8.15 tests=[BAYES_99=2.5, FORGED_RCVD_HELO=0.135], autolearn=no, quarantine m6lWPoTGJ2O (spam-quarantine) + #TD SPAM, -> , Yes, score=17.887 tag=-10 tag2=6.31 kill=6.31 tests=[BAYES_99=4], autolearn=spam, quarantine VFYjDOVTW4zd (spam-quarantine) + #TD SPAM-TAG, -> , No, score=-0.069 tagged_above=-10 required=6.31 tests=[BAYES_00=-2.599, FROM_ENDS_IN_NUMS=2.53] + #TD SPAM-TAG, -> , No, score=-1.294 required=8.15 tests=[BAYES_00=-2.599, FROM_LOCAL_HEX=1.305] + # pre 2.3.3 + #TD SPAM-TAG, -> , Yes, hits=6.159 tagged_above=-999 required=3.4 tests=BAYES_99=3.5, FUZZY_CPILL=0.518, HTML_MESSAGE=0.001, URIBL_WS_SURBL=2.14 + #TD SPAM, -> , Yes, hits=8.1 tag1=-999.0 tag2=7.0 kill=7.0 tests=MANGLED_TAKE, UPPERCASE_25_50, quarantine spam-14156-09 (maia-spam-quarantine) + + $Totals{'tagged'}++ if uc($tagtype) eq 'SPAM-TAG'; + + if ($tests) { + my $type = $isspam =~ /^Y/ ? 'Spam' : 'Ham'; + + # Note: A SPAM line may be followed by an almost identical SPAM-TAG line. To avoid double counting, + # maintain a list of (abbreviated) SPAM tag lines keyed by pid. Since pid's are recycled, + # maintain an approximation of uniqueness by combining several components from the log + # line (we can't use the date information, as in logwatch, it is not present). + # XXX: It is safe to delete an entry when the final Passed/Block line occurs + + #TD SPAM, -> , Yes, score=34.939 tag=x tag2=6.31 kill=6.31 tests=[DATE_IN_FUTURE_03_06=1.961], autolearn=disabled + #TD SPAM-TAG, -> , Yes, score=34.939 required=6.31 tests=[DATE_IN_FUTURE_03_06=1.961] + #TD SPAM, -> tod@sample.net>, Yes, score=31.565 tag=x tag2=6.9 kill=6.9 tests=[AV:Sanesecurity.Phishing.Bank.2666.UNOFFICIAL=4.1, AV:Sanesecurity.Phishing.Bank.2666.UNOFFICIAL=4.1, BAYES_99=4, DCC_CHECK=4, DIGEST_MULTIPLE=0.001, FORGED_MUA_OUTLOOK=3.116, FORGED_OUTLOOK_HTML=0.001, FORGED_OUTLOOK_TAGS=0.001, HTML_MESSAGE=0.001, L_AV_SS_Phish=5, MIME_HTML_ONLY=1.457, NORMAL_HTTP_TO_IP=0.001, RAZOR2_CF_RANGE_51_100=2, RAZOR2_CF_RANGE_E4_51_100=1.5, RAZOR2_CF_RANGE_E8_51_100=1.5, RAZOR2_CHECK=3, RDNS_NONE=0.1, URIBL_PH_SURBL=1.787] autolearn=spam + + + my $tagstr = $fromto . '/' . $isspam . '/' . $tests; + if (uc($tagtype) eq 'SPAM-TAG' and exists $spamtags{$pid}) { + next if ($spamtags{$pid} eq $tagstr); + } + $spamtags{$pid} = $tagstr; + + #for (split /=[^,]+(?:, +|$)/, $tests) + # amavis < 2.6.2 would double list AV names when using + # @virus_name_to_spam_score_maps. + my @unique_tests = unique_list (split /, +/, $tests); + for (@unique_tests) { + # skip possible trailing junk ("quarantine, ...") when older non-bracked tests=xxx is used + next if ! /[^=]+=[\-.\d]+/; + my ($id,$val) = split /=/; + if ($id =~ /^BAYES_\d+$/) { + $Counts{'bayes'}{$id}++ if ($Collecting{'bayes'}); + } + if ($Opts{'sarules'}) { + if ($id eq 'DKIM_POLICY_SIGNSOME') { $val = 0 } + elsif ($id eq 'AWL') { $val = '-' } + $Counts{'sarules'}{$type}{sprintf "%6s %s", $val, $id}++; + } + } + # Handled below + #autolearn= is available only at ll>=3 or SPAM messages; so ham doesn't naturally occur here + # SA 2.5/2.6 : ham/spam/no + # SA 3.0+ : ham/spam/no/disabled failed/unavailable + #$Counts{'autolearn'}{$type}{$autolearn}++ if ($Opts{'autolearn'}); + } + } + + # Passed or Blocked + elsif ($p1 =~ /^(Passed|Blocked)(.*)/) { + $action = lcfirst $1; + ($p1 = $2) =~ s/^\s+//; + + $p1 =~ s/^,/CLEAN,/; # canonicalize older log entries + #print "P1: \"$p1\"\n"; + + # amavis 20030616p10-5 + #TD Passed, -> , Message-ID: <652.44494541@example.com>, Hits: 4.377 + #TD Passed, -> , Message-ID: , Hits: - + #TD Passed, -> , quarantine IJHkgliCm2Ia, Message-ID: <20080307140552.16E127641E@example.com>, Hits: 0.633 + + #TD Passed CLEAN, [10.0.0.1] [10.0.0.1] -> , Message-ID: <2qxz191@example.com>, mail_id: w4DHD8, Hits: -2.599, size: 3045, queued_as: 2056, 2664 ms + #TD Passed CLEAN, [10.0.0.1] [10.0.0.1] -> , Message-ID: <2qxz191@example.com>, mail_id: w4DHD8, Hits: -2.541-3, size: 3045, queued_as: 2056, 2664 ms + #TD Blocked SPAM, [10.0.0.1] [192.168.0.1] -> , quarantine: spam-EzEbE9W, Message-ID: <117894@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms + #TD Blocked SPAM, LOCAL [10.0.0.1] [10.0.0.2] -> , quarantine: spam-EzEbE9W, Message-ID: <110394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms + #TD Blocked SPAM, [IPv6:2001:630:d0:f102:230:48ff:fe77:96e] [192.168.0.1] -> , quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms + #TD Passed SPAMMY, ORIGINATING/MYNETS LOCAL [10.0.0.1] [10.0.0.1] -> ,, quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms + #TD Blocked SPAM, B-BANK/C-BANK/B-BANK [10.0.0.1] [10.0.0.1] -> , quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms + #TD Blocked SPAM, [10.0.0.1] [10.0.0.1] -> , quarantine: spam-AV49p5, Message-ID: <1.007@sample.net>, mail_id: AV49p5, Hits: 7.487, size: 27174, 4406 ms + #TD Passed SPAM, MYNETS -> , quarantine: spam-V3Wq, Message-ID: <220.1B@example.com>, mail_id: V3Wq, Hits: 7, size: 8838, queued_as: C63EC, 18 ms + #TD Passed SPAM, <> -> <"fred).flintstone"@domain.tld>, Message-ID: <200801180104.CAA23669@aserver.sub.adomain.tld>, mail_id: 6AzQ1g0l5RgP, Hits: 9.061, size: 5555, queued_as: C1840506CB8, 8766 ms + #TD Blocked INFECTED (HTML.Phishing.Bank-43), [198.168.0.1] [10.0.0.1] -> , quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Blocked INFECTED (Trojan.Downloader.Small-9993), LOCAL [10.0.0.2] [10.0.0.2] -> , quarantine: virus-SCwJcs, Message-ID: <9009@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Blocked BANNED (multipart/report | message/partial,.txt), [192.168.0.1] [10.0.0.2] <> -> , quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Blocked BANNED (multipart/report | message/partial,.txt), LOCAL [192.168.0.1] [10.0.0.2] <> -> , quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Blocked BANNED (multipart/mixed | application/octet-stream,.asc,=?iso-8859-1?Q?FTP=5FFile=5F (1)=File(1).reg), [192.168.0.0] [192.168.0.0] -> , quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Blocked BANNED (multipart/related | application/zip,.zip,card.zip | .exe,.exe-ms,Card.exe), [10.0.0.2] [10.0.0.2] -> , quarantine: banned-9OXm4Q3ah, Message-ID: <08517$@from>, mail_id: 9OXm4Q3ah, Hits: -, size: 2366, 3803 ms + #TD Passed BAD-HEADER, [192.168.0.1] [10.0.0.2] -> , quarantine: virus-SCwJcs, Message-ID: , mail_id: SCwJcs, Hits: 2.54 size: 4134, 3721 ms + #TD Passed BAD-HEADER, LOCAL [192.168.0.1] [10.0.0.2] -> , quarantine: virus-SCwJcs, Message-ID: , mail_id: SCwJcs, Hits: 3.2 size: 4134, 3721 ms + #TD Passed BAD-HEADER, MYNETS AM.PDP [127.0.0.1] [127.0.0.1] -> , quarantine: virus-SCwJcs, Message-ID: , mail_id: SCwJcs, Hits: 1.2 size: 4134, 3721 ms + #TD Passed BAD-HEADER, ORIGINATING/MYNETS LOCAL [10.0.0.1] [10.0.0.1] -> ,,, quarantine: virus-SCwJcs, Message-ID: , mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms + #TD Passed BAD-HEADER, [10.0.0.1] [10.0.0.2] -> , quarantine: badh-lxR, Message-ID: <7fm@example.com>, mail_id: lxR, Hits: -2.292, size: 422, queued_as: E3B, 981 ms + #TD Passed UNCHECKED, MYNETS LOCAL [192.168.0.1] [192.168.0.1] -> Message-ID: <002e01c759c7$5de437b0$0a02a8c0@somehost>, mail_id: 7vtR-7BAvHZV, Hits: -, queued_as: B5420C2E10, 6585 ms + #TD Blocked MTA-BLOCKED, LOCAL [192.168.0.1] [192.168.0.2] -> , Message-ID: <438548@example.com>, mail_id: tfgTCiyvFw, Hits: -2.54, size: 4895, 31758 ms + #TD Blocked OVERSIZED, LOCAL [10.0.0.1] [10.0.0.1] -> , Message-ID: <435@example.com>, mail_id: tfTivFw, Hits: -2.54, size: 444444895, 31758 ms + #TD Blocked OTHER, LOCAL [10.0.0.1] [10.0.0.1] -> , Message-ID: <435@example.com>, mail_id: tfTivFw, Hits: -2.54, size: 495, 31758 ms + #TD Blocked TEMPFAIL, [10.0.0.2] [10.0.0.1] -> , Message-ID: <200703302301.9f1899470@example.com>, mail_id: bgf52ZCNbPo, Hits: -2.586, 3908 ms + + #2.3.1 + #<>,,Passed,Hits=-3.3,Message-ID=<200506440.1.sample.net>,Size=51458 + #20030616p10-5 + #Not-Delivered, -> , quarantine spam-ea32770-03, Message-ID: , Hits: 9.687 + + # malwarepassed, malwareblocked + # xxx very old + # Virus found - quarantined| + #amavisd-new-20030616 + # INFECTED (JS/IllWill-A), -> , quarantine virus-20040811-207-0-03, Message-ID: <0440.5577-101@sample.net>, Hits: - + # INFECTED (Exploit.HTML.IFrame, Worm.SomeFool.P), -> ,, quarantine qiO2ZG4K, Message-ID: <200608.5A5@mail.example.com>, Hits: - + #XXX (?:(Passed|Blocked) )?INFECTED \(([^\)]+)\),[A-Z .]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[)>]/o )) + #XXX elsif (($action, $key, $ip, $from, $to) = ( $p1 =~ /^(?:Virus found - quarantined|(?:(Passed|Blocked) )?INFECTED) \(([^\)]+)\),[A-Z .]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[(>]/o )) + + # the first IP is the envelope sender. + if ($p1 !~ /^(CLEAN|SPAM(?:MY)?|INFECTED \(.*?\)|BANNED \(.*?\)|BAD-HEADER(?:-\d)?|UNCHECKED|MTA-BLOCKED|OVERSIZED|OTHER|TEMPFAIL)(?: {[^}]+})?, ([^[]+ )?(?:([^<]+) )?[<(](.*?)[>)] -> ([(<].*?[)>]), (?:.*Hits: ([-+.\d]+))(?:.* size: (\d+))?(?:.* autolearn=(\w+))?/) { + inc_unmatched('passblock'); + next; + } + my $trigger; + my ($ccatmajor, $pbanks, $ips, $from, $reciplist, $hits, $size, $autolearn) = ($1, $2, $3, $4, $5, $6, $7, $8); + + $Totals{'bytesscanned'} += $size if defined $size; + + #print "ccatmajor: \"$ccatmajor\", pbanks: \"$pbanks\"\n"; + if ($ccatmajor =~ /^(INFECTED|BANNED) \((.*)\)$/) { + ($ccatmajor, $trigger) = ($1, $2); + #print "\tccatmajor: \"$ccatmajor\", trigger: \"$trigger\"\n"; + } + + $ccatmajor =~ s/(BAD-HEADER)-\d/$1/; # strip amavis 2.7's [:ccat|minor] BAD-HEADER sub-classification + $sectkey = $ccatmajor_to_sectkey{$ccatmajor} . $action; + $Totals{$sectkey}++; + + # Not checked by spamassassin, due to $sa_mail_body_size_limit or @bypass_spam_checks_maps + if ($hits eq '-') { + # Don't increment sabypassed for INFECTED (SA intentionally not called) + unless ($ccatmajor eq 'INFECTED') { + # The following order is used, the first condition met decides the outcome: + # 1. a virus is detected: mail is considered infected; + # 2. contains banned name or type: mail is considered banned; + # 3. spam level is above kill level for at least one recipient, or a sender is blacklisted: mail is considered spam; + # 4. bad (invalid) headers: mail is considered as having a bad header. + # Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN + $Totals{'sabypassed'}++; + } + } else { + if ($Collecting{'spamscores'}) { + no re 'taint'; + if ($hits =~ /^(-?[.\d]+)([-+])([.\d]+)$/) { + $hits = eval $1.$2.$3; # untaint $hits, to sum $1 and $3 values + } + # SA not called for ccats INFECTED and BANNED (Hits: -). + # UNCHECKED may have a score, so we can't distinguish Ham from Spam + push @{$SpamScores{$ccatmajor_to_spamham{$ccatmajor}}}, $hits; + } + } + + # autolearn is available here only if enabled in amavis template + if ($autolearn ne '' and $Opts{'autolearn'}) { + #if ($autolearn ne '' and ($ccatmajor eq 'SPAM' or $ccatmajor eq 'CLEAN')) { + # SA 2.5/2.6 : ham/spam/no + # SA 3.0+ : ham/spam/no/disabled/failed/unavailable + # printf "INC: autolearn: %s, %s: %d\n", $ccatmajor eq 'SPAM' ? 'Spam' : 'Ham', $autolearn, $Opts{'autolearn'};; + # Priorities other than SPAM will be considered HAM for autolearn stats + $Counts{'autolearn'}{$ccatmajor eq 'SPAM' ? 'Spam' : 'Ham'}{$autolearn}++; + } + + # p0f fingerprinting + if (exists $p0ftags{$pid}) { + my ($ip,$score,$os) = split(/\//, $p0ftags{$pid}); + $Counts{'p0f'}{ucfirst($ccatmajor_to_spamham{$ccatmajor})}{$os}{$ip}++; + #print "Deleting p0ftag: $pid\n"; + delete $p0ftags{$pid}; + } + + next unless ($Collecting{$sectkey}); + # cleanpassed never gets here... + + # prefer xforward IP if it exists + # $ip_a => %a original SMTP session client IP address (empty if unknown, e.g. no XFORWARD) + # $ip_e => %e best guess of the originator IP address collected from the Received trace + my ($ip_a, $ip_e) = split(/ /, $ips, 2); + + $ip = $ip_a ? $ip_a : $ip_e; + $ip =~ s/[[\]]//g; + #print "ip: \"$ip\", ip_a: \"$ip_a\", ip_e: \"$ip_e\", from: \"$from\", reciplist: \"$reciplist\"; hits: \"$hits\"\n"; + $ip = '*unknown IP' if ($ip eq ''); + $from = '<>' if ($from eq ''); + + # Show first recipient only, or all + my @recips = split /,/, $reciplist; + @recips = map { /^<(.+)>$/ } @recips; + # show only first recipient + $to = lc ($Opts{'first_recip_only'} ? $recips[0] : "@recips"); + + if ($ccatmajor eq 'INFECTED') { # $ccatmajor: INFECTED malwarepassed, malwareblocked + $Counts{$sectkey}{$trigger}{$to}{$ip}{$from}++; + } + elsif ($ccatmajor eq 'BANNED') { # $ccatmajor: BANNED bannednamepassed, bannednameblocked + $Counts{$sectkey}{$to}{$trigger}{$ip}{$from}++; + } else { + # $ccatmajor: CLEAN | SPAM{MY} | BAD-HEADER | UNCHECKED | MTA-BLOCKED | OVERSIZED | OTHER | TEMPFAIL + # cleanpassed, cleanblocked, spampassed, spamblocked, badheaderpassed, badheaderblocked + # uncheckedpassed, uncheckblocked, mtapassed, mtablocked, oversizedpassed, oversizedblocked + # otherpassed, otherblocked, tempfailpassed, tempfailblocked + $Counts{$sectkey}{$to}{$ip}{$from}++; + } + + # old... + #XXX elsif (($action, $item, $ip, $from, $to) = ( $p1 =~ /^(?:(Blocked|Passed) )?BANNED (?:name\/type )?\((.+)\),[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[(>]/o)) + #XXXX elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Passed|Blocked) )?UNCHECKED,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^>)]*)[)>]/o )) + #XXX elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Passed|Blocked) )?TEMPFAIL,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^>)]*)[)>]/o )) + #XXX elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Blocked|Passed) )?BAD-HEADER,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [(<]([^>)]*)[)>](?: -> [(<]([^>)]+)[)>])[^:]*/o )) + # amavis 2.3.1 + #BAD-HEADER, <> -> , Message-ID: <200506440.1.sample.net>, Hits=-3.3 tag1=3.0 tag2=7.5 kill=7.5, tests=ALL_TRUSTED=-3.3, [10.0.0.1] + } # end Passed or Blocked + + # MAIA + elsif ($p1 =~ /^FAKE SENDER, ([^:]+): ($[^,]+), (.*)$/o) { + #TD FAKE SENDER, SPAM: 192.168.0.1, bogus@example.com + $Totals{'fakesender'}++; next unless ($Collecting{'fakesender'}); + $Counts{'fakesender'}{$1}{$2}{$3}++; + } + + elsif ($p1 =~ /^p\d+ \d+(?:\/\d+)* Content-Type: ([^,]+)(?:, size: [^,]+, name: (.*))?/) { + my ($ts, $name) = ($1, $2); + #TD p006 1 Content-Type: multipart/mixed + #TD p008 1/1 Content-Type: multipart/signed + #TD p001 1/1/1 Content-Type: text/plain, size: 460 B, name: + #TD p002 1/1/2 Content-Type: application/pgp-signature, size: 189 B, name: + #TD p002 1/2 Content-Type: application/octet-stream, size: 3045836 B, name: abc.pdf + next unless ($Collecting{'contenttype'}); + my ($type, $subtype) = $ts !~ '""' ? split /\//, $ts : ('unspecified', 'unspecified'); + + $name = '' if !defined $name or $name =~ /^\s*$/; + $Counts{'contenttype'}{$type}{$subtype}{$name}++; + } + + # LMTP/SMTP connection + # NOTE: no longer used. size data now being obtained from Passed/Block line, as size info may not be available here + #elsif (my ($size) = ($p1 =~ /^[LS]MTP:(?:\[$re_IP\])?:\d+ [^:]+: [<(](?:.*?)[>)] -> \S+ (?:SIZE=(\d+))?.*?Received: / )) { + elsif ($p1 =~ /^[LS]MTP:/) { + #TD LMTP::10024 /var/spool/amavis/tmp/amavis-20070119T144757-09086: -> SIZE=1000 Received: from mail.sample.net ([127.0.0.1]) by localhost (mail.sample.net [127.0.0.1]) (amavisd-new, port 10024) with LMTP for ; Fri, 19 Jan 2007 15:41:45 -0800 (PST) + #TD SMTP:[127.0.0.1]:10024 /var/spool/amavis/tmp/amavis-20070119T144757-09086: -> , SIZE=2500000 Received: from mail.sample.net ([127.0.0.1]) by localhost (mail.sample.net [127.0.0.1]) (amavisd-new, port 10024) with LMTP for ; Fri, 19 Jan 2007 15:41:45 -0800 (PST) + #TD SMTP::10024 /var/lib/amavis/tmp/amavis-27-26927: -> Received: from localhost ([127.0.0.1]) by localhost (example.com [127.0.0.1]) (amavisd-new, port 10024) with SMTP for ; Sat, 7 Jun 2008 23:09:34 +0200 (CEST) + #$Totals{'bytesscanned'} += $size if defined $size; + } + + #(\S+) ([^[(]+)(.*)$ + elsif ($p1 =~ /^OS_fingerprint: (\S+) ([-\d.]+) (\S+)(?: ([^[(]+|\[[^]]+\]))?/o) { + #TD OS_fingerprint: 213.193.24.113 29.789 Linux 2.6 (newer, 1) (up: 1812 hrs), (distance 14, link: ethernet/modem) + #TD OS_fingerprint: 10.47.2.155 -1.312 MYNETWORKS + # Note: safe to delete entry when the final Passed/Block line occurs + if ($Collecting{'p0f'}) { + my ($genre,$vers) = ($3,$4); + #print "p0f:\t$3\t\t$vers\n"; + if ($genre eq 'Windows') { + local($1); + $vers = $1 if $vers =~ /^(\S+) /; + $genre .= ' ' . $vers; + } + elsif ($genre eq 'UNKNOWN') { + $genre = 'Unknown'; + } + $p0ftags{$pid} = join('/', $1,$2,$genre); + #print "Added PID: $pid, $p0ftags{$pid}\n"; + } + } + + elsif ( ($reason) = ( $p1 =~ /^BAD HEADER from [^:]+: (.+)$/) or + ($reason) = ( $p1 =~ /check_header: \d, (.+)$/)) { + # When log_level > 1, provide additional header or MIME violations + + # amavisd < 2.4.0, log_level >= 1 + #TD BAD HEADER from : Improper use of control character (char 0D hex) in message header 'Received': Received: example.com[10.0.0.1\r] + #TD BAD HEADER from : Non-encoded 8-bit data (char F7 hex) in message header 'Subject': Subject: \367\345\370\361 \344\351\351\362\345\365\n + #TD BAD HEADER from : MIME error: error: part did not end with expected boundary + #TD BAD HEADER from (bulk ) : Non-encoded 8-bit data (char E6 hex) in message header 'Subject': Subject: spam\\346ham\\n + #TD BAD HEADER from (list) : MIME error: error: part did not end with expected boundary + # amavisd >= 2.4.3, log_level >= 2 + #TD check_header: 2, Non-encoded 8-bit data (char AE hex): Subject: RegionsNet\\256 Online Banking\\n + #TD check_header: 2, Non-encoded 8-bit data (char E1 hex): From: "any user" \\n + #TD check_header: 3, Improper use of control character (char 0D hex): Content-type: text/html; charset=i... + #TD check_header: 8, Duplicate header field: "Reply-To" + #TD check_header: 8, Duplicate header field: "Subject" + #TD check_header: 4, Improper folded header field made up entirely of whitespace (char 09 hex): X-Loop-Detect: 3\\n\\t\\n + #TD check_header: 4, Improper folded header field made up entirely of whitespace: Received: ...8 ; Thu, 10 Jan 2008 03:41:35 +0100\\n\\t \\n + + + my $subreason; + if ($reason =~ /^(.*?) \((char \S+ hex)\)(.*)$/) { + $reason = $1; + my ($char,$sub) = ($2,$3); + + $sub =~ s/^in message header '[^:]+': //; + $sub =~ s/^: //; + $subreason = "$char: $sub"; + } + elsif ($reason =~ /^(Improper folded header field made up entirely of whitespace):? (.*)/) { + $reason = $1; + $subreason = $2; + } + elsif ($reason =~ /^(Duplicate header field): "(.+)"$/) { + $reason = $1; + $subreason = $2; + } + elsif ($reason =~ /^(MIME error): (?:error: )?(.+)$/) { + $reason = $1; + $subreason = $2; + } + + $Totals{'badheadersupp'}++; next unless ($Collecting{'badheadersupp'}); + $Counts{'badheadersupp'}{$reason}{$subreason}++; + } + + elsif ($p1 =~ /^truncating a message passed to SA at/) { + #TD truncating a message passed to SA at 431018 bytes, orig 1875912 + $Totals{'truncatedmsg'}++; + } + + elsif ($p1 =~ /: spam level exceeds quarantine cutoff level/ or + $p1 =~ /: cutoff, blacklisted/) { + #TD do_notify_and_quarantine: spam level exceeds quarantine cutoff level 20 + #TD do_notify_and_quarantine: cutoff, blacklisted + $Totals{'spamdiscarded'}++; + } + + elsif ( $p1 =~ /^spam_scan: (.*)$/) { + #if ($1 =~ /^not wasting time on SA, message longer than/ ) { + #TD spam_scan: not wasting time on SA, message longer than 409600 bytes: 1326+4115601 + # this causes duplicate counts, and the subsequent Passed/Blocked log line + # will have "Hits: -," whereby sabypassed is incremented. + #$Totals{'sabypassed'}++; + #} + # ignore other spam_scan lines + } + + # WARN: + elsif ( ($reason) = ( $p1 =~ /^WARN: MIME::Parser error: (.*)$/ )) { + # WARN: MIME::Parser error: unexpected end of header + $Totals{'mimeerror'}++; next unless ($Collecting{'mimeerror'}); + $Counts{'mimeerror'}{$reason}++; + } + + elsif ($p1 =~ /^WARN: address modified \((\w+)\): <(.*?)> -> <(.*)>$/) { + #TD WARN: address modified (sender): -> + #TD WARN: address modified (recip): -> + #TD WARN: address modified (recip): -> + #TD WARN: address modified (recip): <"test@example.com"@> -> <"teszt@example.com"> + #TD WARN: address modified (sender): -> <"fr\344om"@sample.net> + $Totals{'warningaddressmodified'}++; next unless ($Collecting{'warningaddressmodified'}); + $Counts{'warningaddressmodified'}{$1 eq 'sender' ? "Sender address" : "Recipient address"}{"$2 -> $3"}++; + } + + # NOTICE: + elsif ($p1 =~ /^NOTICE: (.*)$/) { + # uninteresting + #TD NOTICE: reconnecting in response to: err=2006, HY000, DBD::mysql::st execute failed: MySQL server has gone away at (eval 71) line 166, line 4. + next if ($1 =~ /^Disconnected from SQL server/); # redundant + next if ($1 =~ /^do_search: trying again: LDAP_OPERATIONS_ERROR/); + next if ($1 =~ /^reconnecting in response to: /); + + + if ($1 =~ /^Not sending DSN, spam level ([\d.]+ )?exceeds DSN cutoff level/) { + #TD NOTICE: Not sending DSN, spam level exceeds DSN cutoff level for all recips, mail intentionally dropped + $Totals{'dsnsuppressed'}++; + $Counts{'dsnsuppressed'}{'DSN cutoff exceeded'}++; + } + elsif ($1 =~ /^Not sending DSN to believed-to-be-faked sender/) { + #TD NOTICE: Not sending DSN to believed-to-be-faked sender , mail containing VIRUS intentionally dropped + $Totals{'dsnsuppressed'}++; + $Counts{'dsnsuppressed'}{'Sender likely faked'}++; + } + elsif ($1 =~ /^DSN contains [^;]+; bounce is not bounc[ai]ble, mail intentionally dropped/) { + $Totals{'dsnsuppressed'}++; + $Counts{'dsnsuppressed'}{'Not bounceable'}++; + } + elsif ($1 =~ /^UNABLE TO SEND DSN to /) { + #TD NOTICE: UNABLE TO SEND DSN to : 554 5.7.1 Failed, id=19838-01, from MTA([127.0.0.1]:10025): 554 5.7.1 : Recipient address rejected: Access denied + $Totals{'dsnsuppressed'}++; + $Counts{'dsnsuppressed'}{'Unable to send'}++; + } + + elsif ($1 =~ /^Skipping (?:bad|extra) output from file\(1\)/) { + #TD NOTICE: Skipping extra output from file(1): blah + #TD NOTICE: Skipping bad output from file(1) at [1, p002], got: blah + $Totals{'fileoutputskipped'}++; + } + elsif (($p1) = ($1 =~ /^Virus scanning skipped: (.*)$/)) { + #TD NOTICE: Virus scanning skipped: Maximum number of files (1500) exceeded at (eval 57) line 1283, line 1501. + $Totals{'virusscanskipped'}++; next unless ($Collecting{'virusscanskipped'}); + $Counts{'virusscanskipped'}{strip_trace($p1)}++; + } + else { + inc_unmatched('NOTICE'); + next; + } + } + + # INFO: + elsif ($p1 =~ /^INFO: (.*)$/) { + next if ($1 =~ /^unfolded \d+ illegal all-whitespace continuation line/); + next if ($1 =~ /^removed bare CR/); + + if ($1 =~ /^truncat(ed|ing)/) { + #TD INFO: truncating long header field (len=2639): X-Spam-Report: =?iso-8859-1?Q?=0A=0A*__1=2E7_SUBJECT=5FENCODED=5FTWICE_Subject=3A_MIME_e?= =?iso-885... + #TD INFO: truncated 1 header line(s) longer than 998 characters + $Totals{'truncatedheader'}++; + } elsif ( $1 =~ /^no existing header field 'Subject', inserting it/) { + $Totals{'nosubject'}++; + } + elsif (my ($savers1, $savers2, $item) = ( $1 =~ /^(?:SA version: ([^,]+), ([^,]+), )?no optional modules: (.+)$/)) { + #TD INFO: SA version: 3.1.8, 3.001008, no optional modules: DBD::mysql Mail::SpamAssassin::Plugin::DKIM Mail::SpamAssassin::Plugin::URIDetail Error + next unless ($Opts{'startinfo'}); + if ($savers1 ne '') { + $StartInfo{'sa_version'} = "$savers1 ($savers2)"; + } + foreach my $code (split / /, $item) { + $StartInfo{'Code'}{'Not loaded'}{$code} = ""; + } + } + elsif (my ($name) = ( $1 =~ /^(unknown banned table name \S+), .+$/)) { + #TD INFO: unknown banned table name 1, recip=r@example.com + $Totals{'warning'}++; next unless ($Collecting{'warning'}); + $Counts{'warning'}{ucfirst $name}++; + } + else { + inc_unmatched('INFO'); + next; + } + } + + elsif ( ($action,$reason,$from,$to) = ($p1 =~ /^DSN: NOTIFICATION: Action:([^,]+), ([^,]+), <(.*?)> -> <(.*?)>/)) { + #TD DSN: NOTIFICATION: Action:failed, LOCAL 554 Banned, -> + #TD DSN: NOTIFICATION: Action:delayed, LOCAL 454 Banned, -> + + $Totals{'dsnnotification'}++; next unless ($Collecting{'dsnnotification'}); + $Counts{'dsnnotification'}{$action}{$reason}{"$from -> $to"}++; + } + + elsif (($item, $from, $to) = ( $p1 =~ /^Quarantined message release(?: \([^)]+\))?: ([^ ]+) <(.*?)> -> (.+)$/) or + ($item, $from, $to) = ( $p1 =~ /^Quarantine release ([^ ]+): overriding recips <([^>]*)> by (.+)$/)) { + #TD Quarantine release arQcr95dNHaW: overriding recips by + #TD Quarantined message release: hiyPJOsD2m9Z -> + #TD Quarantined message release: hiyPJOsD2m9Z <> -> , + # 2.6+ + #TD Quarantined message release (miscategorized): Iu6+0u1voOA -> + $Totals{'released'}++; next unless ($Collecting{'released'}); + $from = '<>' if ($from eq ''); + $to =~ s/[<>]//g; + $Counts{'released'}{"\L$from"}{$to}{$item}++; + } + elsif ($p1 =~ /^Quarantine release ([^:]+): missing X-Quarantine-ID$/) { + #TD Quarantine release 7ejEBC7MThSc: missing X-Quarantine-ID + $Totals{'warningnoquarantineid'}++; next unless ($Collecting{'warningnoquarantineid'}); + $Counts{'warningnoquarantineid'}{$1}++; + } + + elsif ( ($stage,$reason) = ($p1 =~ /^Negative SMTP resp\S* +to ([^:]+): *(.*)$/)) { + #TD Negative SMTP response to data-dot (): 550 5.7.1 Header Spam Rule 4 + $Totals{'smtpresponse'}++; next unless ($Collecting{'smtpresponse'}); + $Counts{'smtpresponse'}{'Negative response'}{$stage}{$reason}++; + } + elsif ( ($stage,$reason) = ($p1 =~ /^smtp resp to ([^:]+): *(.*)$/)) { + #TD smtp resp to NOOP (idle 4799.4 s): 421 4.4.2 nops.overtops.org Error: timeout exceeded + #TD smtp resp to MAIL (pip): 250 2.1.0 Ok + $Totals{'smtpresponse'}++; next unless ($Collecting{'smtpresponse'}); + $stage =~ s/ [\d.]+ s//; + $Counts{'smtpresponse'}{'Response'}{$stage}{$reason}++; + } + + elsif ( ($item) = ($p1 =~ /^response to RCPT TO for <([^>]*)>: "501 Bad address syntax"/)) { + #TD response to RCPT TO for <""@example.com>: "501 Bad address syntax" + $Totals{'badaddress'}++; next unless ($Collecting{'badaddress'}); + $Counts{'badaddress'}{$item}++; + } + + # do_unip: archive extraction + elsif ($p1 =~ s/^do_unzip: \S+, //) { + $Totals{'archiveextract'}++; next unless ($Collecting{'archiveextract'}); + + if ( $p1 =~ s/^\d+ members are encrypted, //) { + #TD do_unzip: p003, 4 members are encrypted, none extracted, archive retained + $Counts{'archiveextract'}{'Encrypted'}{$p1}++; + + } elsif ( $p1 =~ /^zero length members, archive retained/) { + #TD do_unzip: p002, zero length members, archive retained + $Counts{'archiveextract'}{'Empty member'}{''}++; + + } elsif ($p1 =~ s/^unsupported compr\. method: //) { + #TD do_unzip: p003, unsupported compr. method: 99 + $Counts{'archiveextract'}{'Unsupported compression'}{$p1}++; + } + else { + $Counts{'archiveextract'}{'*unknown'}{$p1}++; + } + } + + # do_cabextract: archive extraction + elsif ($p1 =~ s/^do_cabextract: //) { + #TD do_cabextract: can't parse toc line: File size | Date Time | Name + #TD do_cabextract: can't parse toc line: All done, no errors. + $Totals{'archiveextract'}++; next unless ($Collecting{'archiveextract'}); + + if ($p1 =~ /^([^:]+):\s*(.*)/) { + $Counts{'archiveextract'}{"\u$1"}{$2}++; + } else { + $Counts{'archiveextract'}{$p1}{''}++; + } + } + + elsif ($p1 =~ /^(?:\(!\) *)?SA TIMED OUT,/) { + $Totals{'satimeout'}++; + } + + elsif ($p1 =~ /^mangling (.*)$/) { + $p1 = $1; + if ($p1 =~ /^by (.+?) failed: (.+?), mail will pass unmodified$/) { + #TD mangling by altermine failed: SomeText, mail will pass unmodified + $Totals{'defangerror'}++; next unless ($Collecting{'defangerror'}); + $Counts{'defangerror'}{$1}{$2}++; + } + # other mangle message skipped + else { + #TD mangling YES: 1 (orig: 1), discl_allowed=0, -> + #TD mangling by built-in defanger: 1, + next; + } + } + elsif ($p1 =~ /^DEFANGING MAIL: (.+)$/) { + # log_level 1 + #TD DEFANGING MAIL: WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n Exceeded storage quota 5961070 bytes by d... + #TD DEFANGING MAIL: WARNING: bad headers - Improper use of control character (char 0D hex): To: ,\\n\\t + # could use instead... + #do_log(1,"mangling by %s (%s) done, new size: %d, orig %d bytes", $actual_mail_mangle, $mail_mangle, $repl_size, $msginfo->msg_size); + $Totals{'defanged'}++; next unless ($Collecting{'defanged'}); + $Counts{'defanged'}{$1}++; + } + + elsif ($p1 =~ /^PenPalsSavedFromKill [-.\d]+,/) { + #TD PenPalsSavedFromKill 8.269-3.160, -> + $Totals{'penpalsaved'}++; + } + + # I don't know how many variants of time outs there are... I suppose we'll fix as we go + elsif (($p1 =~ /^\(!+\)([^ ]*) is taking longer than \d+ s and will be killed/) or + ($p1 =~ /^\(!+\)(.*) av-scanner FAILED: timed out/) or + ($p1 =~ /^(?:\(!+\))?(.*): timed out/)) + { + #TD (!)/usr/local/bin/uvscan is taking longer than 10 s and will be killed + #TD (!!)NAI McAfee AntiVirus (uvscan) av-scanner FAILED: timed out + #TD ClamAV-clamd: timed out, retrying (1) + #TD (!)Sophie: timed out, retrying (2) + + $Totals{'avtimeout'}++; next unless ($Collecting{'avtimeout'}); + $Counts{'avtimeout'}{$1}++; + } + elsif (($p2) = ($p1 =~ /SMTP shutdown: (.*)$/)) { # log level -1 + #TD SMTP shutdown: Error writing a SMTP response to the socket: Broken pipe at (eval 49) line 836, line 51. + #TD SMTP shutdown: tempdir is to be PRESERVED: /var/amavis/tmp/amavis-20070704T095350-13145 + strip_trace($p2); + if ($p2 =~ /^tempdir is to be PRESERVED: (.*)\/([^\/]+)$/) { + $Totals{'tmppreserved'}++; + $Counts{'tmppreserved'}{$1}{$2}++ if ($Collecting{'tmppreserved'}); + $p2 = "Preserved tempdir in $1"; + } + $Totals{'warningsmtpshutdown'}++; next unless ($Collecting{'warningsmtpshutdown'}); + $Counts{'warningsmtpshutdown'}{ucfirst($p2)}++; + } + + elsif (($p1 =~ /PRESERVING EVIDENCE in (.*)\/([^\/]+)$/) or + ($p1 =~ /tempdir is to be PRESERVED: (.*)\/([^\/]+)$/)) { + #TD (!)TempDir removal: tempdir is to be PRESERVED: /var/amavis/tmp/amavis-20080110T173606-05767 + # log level -1 + #TD PRESERVING EVIDENCE in /var/amavis/tmp/amavis-20070704T111558-14883 + $Totals{'tmppreserved'}++; next unless ($Collecting{'tmppreserved'}); + $Counts{'tmppreserved'}{$1}{$2}++; + } + + elsif ($p1 =~ /^Open relay\? Nonlocal recips but not originating/) { + $Totals{'warningsecurity'}++; + $Counts{'warningsecurity'}{$p1}++ if ($Collecting{'warningsecurity'}); + } + + # keep before general warnings below, so sadiag gets first crack at log + # lines beginning with "(!) ...". + elsif ($p1 =~ /^(?:\(!+\))?\!?SA (warn|info|error): (.*)$/) { + #TD SA warn: FuzzyOcr: Cannot find executable for gocr + my ($level,$msg) = ($1,$2); + + # XXX later, maybe break out stats on FuzzyOcr + # skip "image too small" for now + if ($msg =~ /^FuzzyOcr: Skipping .+, image too small$/) { + #TD SA warn: FuzzyOcr: Skipping ocrad, image too small + #TD SA warn: FuzzyOcr: Skipping ocrad-decolorize, image too small + #$Counts{'sadiags'}{'fuzzyocr'}{'image too small'}++; + next; + } + elsif ($msg =~ /dns: \[\.\.\.\]/) { + #TD SA info: dns: [...] ;; ADDITIONAL SECTION (1 record) + next; + } + # canonicalize some PIDs and IDs + elsif ($msg =~ s/^pyzor: \[\d+\] error/pyzor: [] error/) { + #TD SA info: pyzor: [11550] error: TERMINATED, signal 15 (000f) + } + elsif ($msg =~ /dns: no likely matching queries for id \d+/) { + $msg =~ s/\d+//; + } + elsif ($msg =~ /dns: no callback for id \d+/) { + $msg =~ s/\d+.*$/.../; + } + + # report other SA warn's + $Totals{'sadiags'}++; + next unless ($Collecting{'sadiags'}); + $Counts{'sadiags'}{ucfirst($level)}{$msg}++; + } + + # catchall for most other warnings + elsif (($p1 =~ /^\(!+\)/) or + ($p1 =~ /^TROUBLE/) or + ($p1 =~ /Can't (?:connect to UNIX|send to) socket/) or + ($p1 =~ /: Empty result from /) or + ($p1 =~ /: Error reading from socket: Connection reset by peer/) or + ($p1 =~ /open\(.*\): Permission denied/) or + ($p1 =~ /^_?WARN: /) or + ($p1 =~ /Can't send SIG \d+ to process \[\d+\]: Operation not permitted/) or + ($p1 =~ /(policy protocol: INVALID(?: AM\.PDP)? ATTRIBUTE LINE: .*)$/) or + ($p1 =~ /(DKIM signature verification disabled, corresponding features not available. If not intentional.*)$/) + ) + { + #TD (!)loading policy bank "AM.PDP-SOCK": unknown field "0" + #TD (!!)policy_server FAILED: SQL quarantine code not enabled at (eval 37) line 306, line 4. + #TD (!!)policy_server FAILED: Can't open file /var/spool/amavis/quarantine/spam-CFJYXmeS+FLy: Permission denied at (eval 37) line 330, line 5. + #TD ClamAV-clamd: Empty result from /var/run/clamav/clamd, retrying (1) + #TDdcc open(/var/dcc/map): Permission denied + #TD TROUBLE in check_mail: FAILED: Died at /usr/sbin/amavisd-maia line 2872, line 22. + #TD TROUBLE in check_mail: spam_scan FAILED: DBD::mysql::st execute failed: MySQL server has gone away at /usr/sbin/amavisd-maia line 3786, line 3036. + #TD TROUBLE in process_request: DBD::mysql::st execute failed: MySQL server has gone away at (eval 35) line 258, line 3. + #TD TROUBLE in process_request: DBD::mysql::st execute failed: Lost connection to MySQL server during query at (eval 35) line 258, line 3. + #TD TROUBLE in process_request: Can't call method "disconnect" on an undefined value at /usr/sbin/amavisd-maia line 2895, line 22. + #TD TROUBLE: recipient not done: smtp response ... + #TD (!!)TROUBLE in process_request: Can't create file /var/amavis/tmp/amavis-98/email.txt: File exists at /usr/local/sbin/amavisd line 4774, line 4. + #TD TROUBLE: lookup table is an unknown object: object ... + #TD (!) policy protocol: INVALID ATTRIBUTE LINE: /var/spool/courier/tmp/114528/D967099\n + #TD (!) policy protocol: INVALID AM.PDP ATTRIBUTE LINE: /var/spool/courier/tmp/114528/D967099\n + #TD _WARN: bayes: cannot open bayes databases /var/spool/amavis/.spamassassin/bayes_* R/W: lock failed: Interrupted system call\n + + $p1 =~ s/^\(!+\)s*//; + + if ($p1 =~ /^WARN: (Using cpio instead of pax .*)$/) { + #TD (!)WARN: Using cpio instead of pax can be a security risk; please add: $pax='pax'; to amavisd.conf and check that the pax(1) utility is available on the system! + $Totals{'warningsecurity'}++; + $Counts{'warningsecurity'}{$1}++ if ($Collecting{'warningsecurity'}); + next; + } + + $p1 =~ s/, retrying\s+\(\d+\)$//; + strip_trace($p1); + + # canonicalize variations of the same message + $p1 =~ s/^run_av \(([^,]+), built-in i\/f\)/$1/; + $p1 =~ s/ av-scanner FAILED: CODE\(0x[^)]+\)/:/; + $p1 =~ s/^(.+: Too many retries to talk to \S+) .*/$1/; + + if (($p1 =~ /(\S+): Can't (?:connect|send) to (?:UNIX )?(.*)$/) or + ($p1 =~ /(\S+): (Too many retries to talk to .*)$/)) + { + + #TD (!)ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory, retrying (2) + #TD (!)ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd: Connection refused, retrying (2) + #TD ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd: Connection refused, retrying (1) + #TD ClamAV-clamd: Can't send to socket /var/run/clamav/clamd: Transport endpoint is not connected, retrying (1) + #TD Sophie: Can't send to socket /var/run/sophie: Transport endpoint is not connected, retrying (1) + #TD (!)run_av (Sophie, built-in i/f): Too many retries to talk to /var/run/sophie (timed out) at (eval 55) line 310, line 16. + #TD (!)run_av (ClamAV-clamd, built-in i/f): Too many retries to talk to /var/run/clamav/clamd.socket (Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory) at (eval 52) line 310. + #TD (!!)ClamAV-clamd av-scanner FAILED: CODE(0x804fa08) Too many retries to talk to /var/run/clamav/clamd.socket (Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory) at (eval 52) line 310. at (eval 52) line 511. + #TD (!!)Sophie av-scanner FAILED: CODE(0x814fd24) Too many retries to talk to /var/run/sophie (timed out) at (eval 55) line 310, line 16. at (eval 55) line 511, line 16. + + $Totals{'avconnectfailure'}++; + $Counts{'avconnectfailure'}{$1}{ucfirst($2)}++ if ($Collecting{'avconnectfailure'}); + next; + } + + # simplify or canonicalize variations of the same message + $p1 =~ s/^TROUBLE(:| in) //; + $p1 =~ s/^_?WARN: //; + $p1 =~ s/Can't create file \S+: (.+)$/Can't create file: $1/; + $p1 =~ s/Can't send SIG \d+ to process \[\d+\]/Can't send SIG to process/; + + $Totals{'warning'}++; next unless ($Collecting{'warning'}); + $Counts{'warning'}{$p1}++; + } + + # Begin forced warnings: Keep this code below warning catchall + elsif ($p1 =~ /^lookup_sql: /) { + #TD lookup_sql: 2006, MySQL server has gone away + $Totals{'warningsql'}++; next unless ($Collecting{'warningsql'}); + $Counts{'warningsql'}{'SQL died'}++; + + } elsif (($reason,$item) = ($p1 =~ /^connect_to_sql: ([^']+) '\S+': (.*?)(?: \(\d+\))?$/) or + ($item,$reason) = ($p1 =~ /^lookup_sql_field\((.*)\) \(WARN: (no such field in the SQL table)\)/)) { + #TD connect_to_sql: unable to connect to DSN 'DBI:mysql:maia:sqlhost1.example.com': Lost connection to MySQL server during query + #TD connect_to_sql: unable to connect to DSN 'DBI:mysql:maia:sqlhost2.example.com': Can't connect to MySQL server on 'sqlhost2.example.com' (111) + #TD lookup_sql_field(id) (WARN: no such field in the SQL table), "from@example.com" result=undef + $Totals{'warningsql'}++; next unless ($Collecting{'warningsql'}); + $Counts{'warningsql'}{ucfirst("$reason: $item")}++; + } + # End forced warnings + + # panic + elsif ( ($p2) = ($p1 =~ /^(?:\(!\)\s*)?PANIC, (.*)$/)) { + #TD PANIC, PANIC, SA produced a clone process of [19122], TERMINATING CLONE [19123] + + $Totals{'panic'}++; next unless ($Collecting{'panic'}); + $Counts{'panic'}{$p2}++; + + } + + # fatal + elsif ( $p1 =~ /^Requesting process rundown after fatal error$/) { + #TD Requesting process rundown after fatal error + $Totals{'fatal'}++; next unless ($Collecting{'fatal'}); + $Counts{'fatal'}{$p1}++; + + # DCC + } elsif (($reason) = ($p1 =~ /^(missing message body; fatal error)/) or + ($reason) = ($p1 =~ /^(try to start dccifd)/)) { + $Totals{'dccerror'}++; next unless ($Collecting{'dccerror'}); + $Counts{'dccerror'}{ucfirst($reason)}++; + } + elsif ($p1 =~ /^continue not asking DCC \d+ seconds after failure/) { + $Totals{'dccerror'}++; next unless ($Collecting{'dccerror'}); + $Counts{'dccerror'}{'Continue not asking DCC after failure'}++; + } + elsif ($p1 =~ /^no DCC answer from (\S+) after \d+ ms$/) { + $Totals{'dccerror'}++; next unless ($Collecting{'dccerror'}); + $Counts{'dccerror'}{"No answer from $1"}++; + } + + elsif ( ($reason, $from, $to) = ($p1 =~ /^skip local delivery\((\d+)\): <(.*?)> -> <(.*?)>$/)) { + $Totals{'localdeliveryskipped'}++; next unless ($Collecting{'localdeliveryskipped'}); + $from = '<>' if ($from eq ''); + $reason = $reason == 1 ? "No localpart" : $reason == 2 ? "Local alias is null" : "Other"; + $Counts{'localdeliveryskipped'}{$reason}{$from}{$to}++; + } + + # hard and soft whitelisted/blacklisted + elsif ($p1 =~ /^wbl: (.*)$/) { + # ignore wbl entries, can't think of good way to reliably summarize. + # and 'black or whitelisted by all' makes using by-white or -black list + # groupings impossible + next; +=cut + $p1 = $1; + + # TD wbl: black or whitelisted by all recips + next if ($p1 =~ /^black or whitelisted/); # not clear how to report this, so skip + next if ($p1 =~ /^checking sender/); # ll 4 + next if ($p1 =~ /^(LDAP) query keys/); # ll 5 + next if ($p1 =~ /^(LDAP) recip/); # ll 5 + next if ($p1 =~ /^recip <[^>]*> (?:black|white)listed sender/); # ll 5 + + # lookup order: SQL, LDAP, static + if ($p1 =~ s/^\(SQL\) recip <[^>]*>//) { + next if ($p1 =~ /^, \S+ matches$/); # ll 5 + next if ($p1 =~ /^, rid=/); # ll 4 + next if ($p1 =~ /^ is neutral to sender/); # ll 5 + next if ($p1 =~ /^ (?:white|black)listed sender whitelisted sender <%s>, '. unexpected wb field value + } + #ll2 + # wbl: (SQL) soft-(white|black)listed (%s) sender <%s> => <%s> (rid=%s)', $val, $sender, $recip, $user_id); + # multiple senders: message sender, then "from", etc. + #ll2 + # wbl: soft-(white|black)listed (%s) sender <%s> => <%s>, + + #TD wbl: whitelisted sender + #TD wbl: soft-whitelisted (-3) sender => , recip_key="." + #TD wbl: whitelisted by user@example.com, but not by all, sender , + # wbl: (whitelisted|blacklisted|black or whitelisted by all recips|(white|black)listed by xxx,yyy,... but not by all) sender %s + + if ($p1 =~ /^(?:\(SQL\) )?(?:(soft)-)?((?:white|black)listed)(?: \([^)]+\))? sender <([^>]*)>/) { + my ($type,$list,$sender) = ($1,$2,$3); + $Totals{$list}++; next unless ($Collecting{$list}); + $type = $type ? 'Soft' : 'Hard' ; + my ($localpart, $domainpart) = split (/@/, lc $sender); + ($localpart, $domainpart) = ($sender, '*unspecified') if ($domainpart eq ''); + $Counts{$list}{$type}{$domainpart}{$localpart}++; + } + else { + inc_unmatched('wbl'); + next; + } +=cut + } + + # XXX: WHITELISTED or BLACKLISTED should be caught in SPAM tag above + elsif (($p1 =~ /^white_black_list: whitelisted sender/) or + ($p1 =~ /.* WHITELISTED/) ) { + $Totals{'whitelisted'}++; + + } elsif (($p1 =~ /^white_black_list: blacklisted sender/) or + ( $p1 =~ /.* BLACKLISTED/) ) { + $Totals{'blacklisted'}++; + + } elsif ($p1 =~ /^Turning AV infection into a spam report: score=([^,]+), (.+)$/) { + #TD Turning AV infection into a spam report: score=4.1, AV:Sanesecurity.ScamL.375.UNOFFICIAL=4.1 + #TD Turning AV infection into a spam report: score=3.4, AV:Sanesecurity.Phishing.Cur.180.UNOFFICIAL=3.1,AV:Sanesecurity.Phishing.Cur.180.UNOFFICIAL=3.4 + #BAT.Backdoor.Poisonivy.E178-SecuriteInfo.com + + next unless ($Collecting{'malwaretospam'}); + #my $score_max = $1; + my @list = split (/,/, $2); + @list = unique_list(\@list); + foreach (@list) { + my ($name,$score) = split (/=/,$_); + $name =~ s/^AV://; + my $type = $name =~ s/\.UNOFFICIAL$// ? 'Unofficial' : 'Official'; + # strip trailing numeric variant (...Phishing.Cur.863) + my $variant = $name =~ s/([.-]\d+)$// ? $1 : '*invariant'; + $Counts{'malwaretospam'}{$type}{$name}{$variant}{$score}++ + } + + # The virus_scan line reports only the one virus name when more than one scanner detects a virus. + # Use instead the ask_av and run_av lines (see below) + # + #} elsif ( my ($malware, $scanners) = ($p1 =~ /virus_scan: \(([^)]+)\), detected by \d+ scanners: (.*)$/ )) { + #TD virus_scan: (HTML.Phishing.Bank-43), detected by 1 scanners: ClamAV-clamd + #TD virus_scan: (Worm.SomeFool.D, Worm.SomeFool.D), detected by 1 scanners: ClamAV-clamd + #TD virus_scan: (Trojan.Downloader.Small-9993), detected by 2 scanners: ClamAV-clamd, NAI McAfee AntiVirus (uvscan) + # foreach (split /, /, $scanners) { + # #$Totals{'malwarebyscanner'}++; # No summary output: redundant w/malwarepassed,malwareblocked} + # $Counts{'malwarebyscanner'}{"$_"}{$malware}++; + # } + + } elsif ($p1 =~ /^(?:ask_av|run_av) (.*)$/) { + next unless ($Collecting{'malwarebyscanner'}); + + if (my ($scanner, $name) = ($1 =~ /^\((.+)\):(?: [^:]+)? INFECTED: ([^,]+)/)) { + #TD ask_av (ClamAV-clamd): /var/amavis/tmp/amavis-20070830T070403-13776/parts INFECTED: Email.Malware.Sanesecurity.07082700 + #TD run_av (NAI McAfee AntiVirus (uvscan)): INFECTED: W32/Zhelatin.gen!eml, W32/Zhelatin.gen!eml + my $type = $name =~ s/\.UNOFFICIAL$// ? 'Unofficial' : 'Official'; + my $variant = ''; + if ($name =~ s/([.-]\d+)$//) { # strip trailing numeric variant (...Phishing.Cur.863) + $variant = $1; + } + $Counts{'malwarebyscanner'}{$scanner}{$type}{$name}{$variant}++; + } + # currently ignoring other ask_av or run_av lines + } + + # Extra Modules loaded at runtime + #TD extra modules loaded after daemonizing/chrooting: Mail/SPF/Query.pm + elsif (($item) = ( $p1 =~ /^extra modules loaded(?: after daemonizing(?:\/chrooting)?)?: (.+)$/)) { + #TD extra modules loaded: PerlIO.pm, PerlIO/scalar.pm + foreach my $code (split /, /, $item) { + #TD extra modules loaded: unicore/lib/gc_sc/Digit.pl, unicore/lib/gc_sc/SpacePer.pl + # avoid useless reporting of pseudo-modules which can't be pre-loaded once + unless ($code =~ m#^unicore/lib/#) { + $Totals{'extramodules'}++; + $Counts{'extramodules'}{$code}++ if ($Collecting{'extramodules'}); + } + } + + # Timing report + } elsif (my ($total,$report) = ( $p1 =~ /^(?:size: \d+, )?TIMING \[total (\d+) ms(?:, [^]]+)?\] - (.+)$/)) { + next if ($report =~ /^got data/); # skip amavis release timing + #TD TIMING [total 5808 ms] - SMTP greeting: 5 (0%)0, SMTP LHLO: 1 (0%)0, SMTP pre-MAIL: 2 (0%)0, SMTP pre-DATA-flush: 5 (0%)0, SMTP DATA: 34 (1%)1, check_init: 1 (0%)1 + # older format, maia mailguard + #TD TIMING [total 3795 ms] - SMTP EHLO: 1 (0%), SMTP pre-MAIL: 0 (0%), maia_read_system_config: 1 (0%), maia_get_mysql_size_limit: 0 (0%), SA check: 3556 (94%), rundown: 0 (0%) + # v2.8.1 + # .... size: 3815, TIMING [total 1901 ms, cpu 657 ms] - ... + + + # Timing line is incomplete - let's report it + if ($p1 !~ /\d+ \(\d+%\)\d+$/ and $p1 !~ /\d+ \(\d+%\)$/) { + inc_unmatched('timing'); + next; + } + + if ($Opts{'timings'}) { + my @pairs = split(/[,:] /, $report); + while (my ($key,$value) = @pairs) { + #4 (0%)0 + my ($ms) = ($value =~ /^([\d.]+) /); + # maintain a per-test list of timings + push @{$Timings{$key}}, $ms; + shift @pairs; shift @pairs; + } + push @TimingsTotals, $total; + } + + } elsif ((($total,$report) = ( $p1 =~ /^TIMING-SA total (\d+) ms - (.+)$/ )) or + (($total,$report) = ( $p1 =~ /^TIMING-SA \[total (\d+) ms, cpu \d+ ms\] - (.+)$/ ))) { + #TIMING-SA [total 3219 ms, cpu 432 ms] - parse: 6 (0.2%), ext + #TD TIMING-SA total 5478 ms - parse: 1.69 (0.0%), extract_message_metadata: 16 (0.3%), get_uri_detail_list: 2 (0.0%), tests_pri_-1000: 25 (0.4%), tests_pri_-950: 0.67 (0.0%), tests_pri_-900: 0.83 (0.0%), tests_pri_-400: 19 (0.3%), check_bayes: 17 (0.3%), tests_pri_0: 5323 (97.2%), check_spf: 12 (0.2%), poll_dns_idle: 0.81 (0.0%), check_dkim_signature: 1.50 (0.0%), check_razo r2: 5022 (91.7%), check_dcc: 192 (3.5%), check_pyzor: 0.02 (0.0%), tests_pri_500: 9 (0.2%), tests_pri_1000: 24 (0.4%), total_awl: 23 (0.4%), check_awl: 10 (0.2%), update_awl: 8 (0.1%), learn: 36 (0.7%), get_report: 1.77 (0.0%) + + # Timing line is incomplete - let's report it + if ($p1 !~ /[\d.]+ \([\d.]+%\)[\d.]+$/ and $p1 !~ /[\d.]+ \([\d.]+%\)$/) { + inc_unmatched('timing-sa'); + next; + } + if ($Opts{'sa_timings'}) { + my @pairs = split(/[,:] /, $report); + while (my ($key,$value) = @pairs) { + #4 (0%)0 + my ($ms) = ($value =~ /^([\d.]+) /); + # maintain a per-SA test list of timings + push @{$TimingsSA{$key}}, $ms; + shift @pairs; shift @pairs; + } + push @TimingsSATotals, $total; + } + + # Bounce killer: 2.6+ + } elsif ($p1 =~ /^bounce (.*)$/) { + #TD bounce killed, -> , from: user@example.com, message-id: , return-path: + #TD bounce rescued by domain, -> , from: user@example.com, message-id: , return-path: + #TD bounce rescued by originating, -> , from: user@example.com, message-id: , return-path: + #TD bounce rescued by: pen pals disabled, -> , from: user@example.com, message-id: , return-path: + $p2 = $1; + + if ($p2 =~ /^killed, <(.+?)> -> /) { + $Totals{'bouncekilled'}++; + $Counts{'bouncekilled'}{$1 eq '' ? '<>' : $1}++ if ($Collecting{'bouncekilled'}); + } + elsif ($p2 =~ /^rescued by ([^,]+), <(.+?)> -> /) { + # note: ignores "rescued by: pen pals disabled" + $Totals{'bouncerescued'}++; + $Counts{'bouncerescued'}{'By ' . $1}{$2 eq '' ? '<>' : $2}++ if ($Collecting{'bouncerescued'}); + } + elsif ($p2 =~ /^unverifiable, <(.+?)> -> /) { + # note: ignores "rescued by: pen pals disabled" + $Totals{'bounceunverifiable'}++; + $Counts{'bounceunverifiable'}{$1 eq '' ? '<>' : $1}++ if ($Collecting{'bounceunverifiable'}); + } + #TD bounce unverifiable, -> + #TD bounce unverifiable, <> -> + } + + # Decoders + elsif (my ($suffix, $info) = ( $p1 =~ /^Internal decoder for (\.\S*)\s*(?:\(([^)]*)\))?$/)) { + #TD Internal decoder for .gz (backup, not used) + #TD Internal decoder for .zip + next unless ($Opts{'startinfo'}); + $StartInfo{'Decoders'}{'Internal'}{$suffix} = $info; + } + + elsif (($suffix, $decoder) = ( $p1 =~ /^No decoder for\s+(\.\S*)\s*(?:tried:\s+(.*))?$/)) { + #TD No decoder for .tnef tried: tnef + # older + #TD No decoder for .doc + next unless ($Opts{'startinfo'}); + $StartInfo{'Decoders'}{'None'}{$suffix} = "tried: " . ($decoder ? $decoder : "unknown"); + } + + elsif (($suffix, $decoder) = ( $p1 =~ /^Found decoder for\s+(\.\S*)\s+at\s+(.*)$/)) { + #TD Found decoder for .bz2 at /usr/bin/bzip2 -d + #TD Found decoder for .bz2 at /usr/bin/7za (backup, not used) + next unless ($Opts{'startinfo'}); + $StartInfo{'Decoders'}{'External'}{$suffix} = exists $StartInfo{'Decoders'}{'External'}{$suffix} ? + join '; ', $StartInfo{'Decoders'}{'External'}{$suffix}, $decoder : $decoder; + } + + # AV Scanners + elsif (my ($tier, $scanner, $location) = ( $p1 =~ /^Found (primary|secondary) av scanner (.+) at (.+)$/)) { + #TD Found primary av scanner NAI McAfee AntiVirus (uvscan) at /usr/local/bin/uvscan + #TD Found secondary av scanner ClamAV-clamscan at /usr/local/bin/clamscan + next unless ($Opts{'startinfo'}); + $StartInfo{'AVScanner'}{"\u$tier"}{$scanner} = $location; + + } elsif (($tier, $scanner, $location) = ( $p1 =~ /^No (primary|secondary) av scanner: (.+)$/)) { + #TD No primary av scanner: CyberSoft VFind + next unless ($Opts{'startinfo'}); + $StartInfo{'AVScanner'}{"\u$tier (not found)"}{$scanner} = ''; + + } elsif ( (($tier, $scanner) = ( $p1 =~ /^Using internal av scanner code for \(([^)]+)\) (.+)$/)) or + (($tier, $scanner) = ( $p1 =~ /^Using (.*) internal av scanner code for (.+)$/))) { + #TD Using internal av scanner code for (primary) ClamAV-clamd + #TD Using primary internal av scanner code for ClamAV-clamd + next unless ($Opts{'startinfo'}); + $StartInfo{'AVScanner'}{"\u$tier internal"}{$scanner} = ''; + + # (Un)Loaded code, protocols, etc. + } elsif (my ($code, $loaded) = ( $p1 =~ /^(\S+)\s+(?:proto? |base |protocol )?\s*(?:code)?\s+((?:NOT )?loaded)$/)) { + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{"\u\L$loaded"}{$code} = ""; + + } elsif (my ($module, $vers) = ( $p1 =~ /^Module (\S+)\s+(.+)$/)) { + #TD Module Amavis::Conf 2.086 + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{'Loaded'}{$module} = $vers; + + } elsif (($module, my $families) = ( $p1 =~ /^socket module (\S+),\s+(.+)$/)) { + #TD socket module IO::Socket::IP, protocol families available: INET, INET6 + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{'Loaded'}{$module} = $families; + + } elsif (($code, $location) = ( $p1 =~ /^Found \$(\S+)\s+at\s+(.+)$/)) { + #TD Found $file at /usr/bin/file + #TD Found $uncompress at /usr/bin/gzip -d + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{'Loaded'}{$code} = $location; + + } elsif (($code, $location) = ( $p1 =~ /^No \$(\S+),\s+not using it/)) { + #TD No $dspam, not using it + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{'Not loaded'}{$code} = $location; + + } elsif (($code, $location) = ( $p1 =~ /^No ext program for\s+([^,]+), (tried: .+)/)) { + #TD No ext program for .kmz, tried: 7za, 7z + #TD No ext program for .F, tried: unfreeze, freeze -d, melt, fcat + next unless ($Opts{'startinfo'}); + $StartInfo{'Code'}{'Not found'}{$code} = $location; + + + } elsif ( $p1 =~ /^starting\.\s+(.+) at \S+ (?:amavisd-new-|Maia Mailguard )([^,]+),/) { + #TD starting. /usr/local/sbin/amavisd at mailhost.example.com amavisd-new-2.5.0 (20070423), Unicode aware, LANG="C" + #TD starting. /usr/sbin/amavisd-maia at vwsw02.eon.no Maia Mailguard 1.0.2, Unicode aware, LANG=en_US.UTF-8 + next unless ($Opts{'startinfo'}); + %StartInfo = () if !exists $StartInfo{'Logging'}; + $StartInfo{'ampath'} = $1; + $StartInfo{'amversion'} = $2; + + } elsif ( $p1 =~ /^config files read: (.*)$/) { + #TD config files read: /etc/amavisd.conf, /etc/amavisd-overrides.conf + next unless ($Opts{'startinfo'}); + $StartInfo{'Configs'} = "$1"; + + } elsif ($p1 =~ /^Creating db in ([^;]+); [^,]+, (.*)$/) { + #TD Creating db in /var/spool/amavis/db/; BerkeleyDB 0.31, libdb 4.4 + next unless ($Opts{'startinfo'}); + $StartInfo{'db'} = "$1\t($2)"; + + } elsif ($p1 =~ /^BerkeleyDB-based Amavis::Cache not available, using memory-based local cache$/) { + #TD BerkeleyDB-based Amavis::Cache not available, using memory-based local cache + next unless ($Opts{'startinfo'}); + $StartInfo{'db'} = "BerkeleyDB\t(memory-based cache: Amavis::Cache unavailable)"; + + } elsif (my ($log) = ($p1 =~ /^logging initialized, log (level \d+, (?:STDERR|syslog: \S+))/)) { + next unless ($Opts{'startinfo'}); + %StartInfo = (); # first amavis log entry, clear out previous start info + $StartInfo{'Logging'} = $log; + + } elsif (( $p1 =~ /^(:?perl=[^,]*, )?user=([^,]*), EUID: (\d+) [(](\d+)[)];\s+group=([^,]*), EGID: ([\d ]+)[(]([\d ]+)[)]/)) { + # uninteresting... + #next unless ($Opts{'startinfo'}); + #$StartInfo{'IDs'}{'user'} = $1; + #$StartInfo{'IDs'}{'euid'} = $2; + #$StartInfo{'IDs'}{'uid'} = $3; + #$StartInfo{'IDs'}{'group'} = $4; + #$StartInfo{'IDs'}{'egid'} = $5; + #$StartInfo{'IDs'}{'gid'} = $6; + } elsif ($p1 =~ /^after_chroot_init: EUID: (\d+) [(](\d+)[)]; +EGID: ([\d ]+)[(]([\d ]+)[)]/) { + #TD after_chroot_init: EUID: 999 (999); EGID: 54322 54322 54322 (54322 54322 54322) + # uninteresting... + + } elsif ($p1 =~ /^SpamAssassin debug facilities: (.*)$/) { + next unless ($Opts{'startinfo'}); + $StartInfo{'sa_debug'} = $1; + + # amavis >= 2.6.3 + } elsif ($p1 =~ /^SpamAssassin loaded plugins: (.*)$/) { + #TD SpamAssassin loaded plugins: AWL, AutoLearnThreshold, Bayes, BodyEval, Check, DCC, DKIM, DNSEval, HTMLEval, HTTPSMismatch, Hashcash, HeaderEval, ImageInfo, MIMEEval, MIMEHeader, Pyzor, Razor2, RelayEval, ReplaceTags, SPF, SpamCop, URIDNSBL, URIDetail, URIEval, VBounce, WLBLEval, WhiteListSubject + next unless ($Opts{'startinfo'}); + map { $StartInfo{'SAPlugins'}{'Loaded'}{$_} = '' } split(/, /, $1); + + } elsif (($p2) = ( $p1 =~ /^Net::Server: (.*)$/ )) { + next unless ($Opts{'startinfo'}); + if ($p2 =~ /^.*starting! pid\((\d+)\)/) { + #TD Net::Server: 2007/05/02-11:05:24 Amavis (type Net::Server::PreForkSimple) starting! pid(4405) + $StartInfo{'Server'}{'pid'} = $1; + } elsif ($p2 =~ /^Binding to UNIX socket file (.*) using/) { + #TD Net::Server: Binding to UNIX socket file /var/spool/amavis/amavisd.sock using SOCK_STREAM + $StartInfo{'Server'}{'socket'} = $1; + } elsif ($p2 =~ /^Binding to TCP port (\d+) on host (.*)$/) { + #TD Net::Server: Binding to TCP port 10024 on host 127.0.0.1 + $StartInfo{'Server'}{'ip'} = "$2:$1"; + } elsif ($p2 =~ /^Setting ([ug]id) to "([^"]+)"$/) { + $StartInfo{'Server'}{$1} = $2; + #TD Net::Server: Setting gid to "91 91" + #TD Net::Server: Setting uid to "91" + } + # skip others + } + + # higher debug level or rare messages skipped last + elsif (! check_ignore_list ($p1, @ignore_list_final)) { + inc_unmatched('final'); + } +} + +######################################## +# Final tabulations, and report printing + + +# spamblocked includes spamdiscarded; adjust here +$Totals{'spamblocked'} -= $Totals{'spamdiscarded'}; + + +#Totals: Blocked/Passed totals +$Totals{'totalblocked'} += $Totals{$_} foreach ( + qw( + malwareblocked + bannednameblocked + uncheckedblocked + spamblocked + spamdiscarded + spammyblocked + badheaderblocked + oversizedblocked + mtablocked + cleanblocked + tempfailblocked + otherblocked + )); + +$Totals{'totalpassed'} += $Totals{$_} foreach ( + qw( + malwarepassed + bannednamepassed + uncheckedpassed + spampassed + spammypassed + badheaderpassed + oversizedpassed + mtapassed + cleanpassed + tempfailpassed + otherpassed + )); + +# Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN +#Totals: Ham/Spam + +$Totals{'totalmalware'} += $Totals{$_} foreach ( + qw(malwarepassed malwareblocked)); + +$Totals{'totalbanned'} += $Totals{$_} foreach ( + qw(bannednamepassed bannednameblocked)); + +$Totals{'totalunchecked'} += $Totals{$_} foreach ( + qw(uncheckedpassed uncheckedblocked)); + +$Totals{'totalspammy'} += $Totals{$_} foreach ( + qw(spammypassed spammyblocked)); + +$Totals{'totalbadheader'} += $Totals{$_} foreach ( + qw(badheaderpassed badheaderblocked)); + +$Totals{'totaloversized'} += $Totals{$_} foreach ( + qw(oversizedpassed oversizedblocked)); + +$Totals{'totalmta'} += $Totals{$_} foreach ( + qw(mtapassed mtablocked)); + +$Totals{'totalclean'} += $Totals{$_} foreach ( + qw(cleanpassed cleanblocked)); + +$Totals{'totalother'} += $Totals{$_} foreach ( + qw(tempfailpassed tempfailblocked otherpassed otherblocked)); + +$Totals{'totalspam'} += $Totals{$_} foreach ( + qw(spampassed spamblocked spamdiscarded totalspammy)); + +# everything lower priority than SPAMMY is considered HAM +$Totals{'totalham'} += $Totals{$_} foreach ( + qw(totalbadheader totaloversized totalmta totalclean)); + +$Totals{'totalmsgs'} += $Totals{$_} foreach ( + qw(totalmalware totalbanned totalunchecked totalspam totalham totalother)); + +# Print the summary report if any key has non-zero data. +# Note: must explicitely check for any non-zero data, +# as Totals always has some keys extant. +# +if ($Opts{'summary'}) { + for (keys %Totals) { + if ($Totals{$_}) { + print_summary_report (@Sections); + last; + } + } +} + +# Print the detailed report, if detail is sufficiently high +# +if ($Opts{'detail'} >= 5) { + print_detail_report (@Sections); + printAutolearnReport; + printSpamScorePercentilesReport; + printSpamScoreFrequencyReport; + printSARulesReport; + printTimingsReport("Scan Timing Percentiles", \%Timings, \@TimingsTotals, $Opts{'timings'}); + printTimingsReport("SA Timing Percentiles", \%TimingsSA, \@TimingsSATotals, 0-$Opts{'sa_timings'}); + printStartupInfoReport if ($Opts{'detail'} >= 10); +} + +#{ +#use Data::Dumper; +#print Dumper(\%p0ftags); +#print Dumper($Counts{'p0f'}); +#} + +# Finally, print any unmatched lines +# +print_unmatched_report(); + +# Evaluates a given line against the list of ignore patterns. +# +sub check_ignore_list($ \@) { + my ($line, $listref) = @_; + + foreach (@$listref) { + return 1 if $line =~ /$_/; + } + + return 0; +} + + +# Spam score percentiles report +# +=pod + ================================================================================== + Spam Score Percentiles 0% 50% 90% 95% 98% 100% + ---------------------------------------------------------------------------------- + Score Spam (100) 6.650 21.906 34.225 36.664 38.196 42.218 + Score Ham (1276) -17.979 -2.599 0.428 2.261 3.472 6.298 + ================================================================================== +=cut +sub printSpamScorePercentilesReport { + return unless ($Opts{'score_percentiles'} and keys %SpamScores); + + #printf "Scores $_ (%d): @{$SpamScores{$_}}\n", scalar @{$SpamScores{$_}} foreach keys %SpamScores; + my (@p, @sorted); + my @percents = split /[\s,]+/, $Opts{'score_percentiles'}; + my $myfw2 = $fw2 - 1; + + print "\n", $sep1 x $fw1, $sep1 x $fw2 x @percents; + printf "\n%-${fw1}s" . "%${myfw2}s%%" x @percents , "Spam Score Percentiles", @percents; + print "\n", $sep2 x $fw1, $sep2 x $fw2 x @percents; + + foreach my $ccat (keys %SpamScores) { + @sorted = sort { $a <=> $b } @{$SpamScores{$ccat}}; + @p = get_percentiles (@sorted, @percents); + printf "\n%-${fw1}s" . "%${fw2}.3f" x scalar (@p), "Score \u$ccat (" . scalar (@sorted) . ')', @p; + } + + print "\n", $sep1 x $fw1, $sep1 x $fw2 x @percents, "\n"; +} + +# Spam score frequency report +# +=pod + ====================================================================================================== + Spam Score Frequency <= -10 <= -5 <= 0 <= 5 <= 10 <= 20 <= 30 > 30 + ------------------------------------------------------------------------------------------------------ + Hits (1376) 29 168 921 170 29 33 1 25 + Percent of Hits 2.11% 12.21% 66.93% 12.35% 2.11% 2.40% 0.07% 1.82% + ====================================================================================================== +=cut +sub printSpamScoreFrequencyReport { + return unless ($Opts{'score_frequencies'} and keys %SpamScores); + + my @scores = (); + push @scores, @{$SpamScores{$_}} foreach (keys %SpamScores); + my $nscores = scalar @scores; + + my @sorted = sort { $a <=> $b } @scores; + my @buckets = sort { $a <=> $b } split /[\s,]+/, $Opts{'score_frequencies'}; + push @buckets, $buckets[-1] + 1; + #print "Scores: @sorted\n"; + + my @p = get_frequencies (@sorted, @buckets); + + my @ranges = ( 0 ) x @buckets; + my $last = @buckets - 1; + $ranges[0] = sprintf "%${fw2}s", " <= $buckets[0]"; + $ranges[-1] = sprintf "%${fw2}s", " > $buckets[-2]"; + for my $i (1 .. @buckets - 2) { + $ranges[$i] = sprintf "%${fw2}s", " <= $buckets[$i]"; + } + + print "\n", $sep1 x $fw1, $sep1 x $fw2 x @buckets; + printf "\n%-${fw1}s" . "%-${fw2}s" x @buckets , "Spam Score Frequency", @ranges; + print "\n", $sep2 x $fw1, $sep2 x $fw2 x @buckets; + printf "\n%-${fw1}s" . "%${fw2}d" x scalar (@p), "Hits ($nscores)", @p; + my $myfw2 = $fw2 - 1; + printf "\n%-${fw1}s" . "%${myfw2}.2f%%" x scalar (@p), "Percent of Hits", map {($_ / $nscores) * 100.0; } @p; + print "\n", $sep1 x $fw1, $sep1 x $fw2 x @buckets, "\n"; +} + +# SpamAssassin rules report +# +=pod + =========================================================================== + SpamAssassin Rule Hits: Spam + --------------------------------------------------------------------------- + Rank Hits % Msgs % Spam % Ham Score Rule + ---- ---- ------ ------ ----- ----- ---- + 1 44 81.48% 93.62% 0.00% 1.961 URIBL_BLACK + 2 44 81.48% 93.62% 14.29% 0.001 HTML_MESSAGE + 3 42 77.78% 89.36% 0.00% 2.857 URIBL_JP_SURBL + 4 38 70.37% 80.85% 14.29% 2.896 RCVD_IN_XBL + 5 37 68.52% 78.72% 0.00% 2.188 RCVD_IN_BL_SPAMCOP_NET + ... + =========================================================================== + + =========================================================================== + SpamAssassin Rule Hits: Ham + --------------------------------------------------------------------------- + Rank Hits % Msgs % Spam % Ham Score Rule + ---- ---- ------ ------ ----- ----- ---- + 1 5 9.26% 2.13% 71.43% 0.001 STOX_REPLY_TYPE + 2 4 7.41% 0.00% 57.14% -0.001 SPF_PASS + 3 4 7.41% 6.38% 57.14% - AWL + 4 1 1.85% 0.00% 14.29% 0.303 TVD_RCVD_SINGLE + 5 1 1.85% 25.53% 14.29% 0.1 RDNS_DYNAMIC + ... + =========================================================================== +=cut +sub printSARulesReport { + return unless (keys %{$Counts{'sarules'}}); + + our $maxlen = 0; + + sub getSAHitsReport($ $) { + my ($type, $topn) = @_; + my $i = 1; + my @report = (); + + return if ($topn eq '0'); # topn can be numeric, or the string "all" + + for (sort { $Counts{'sarules'}{$type}{$b} <=> $Counts{'sarules'}{$type}{$a} } keys %{$Counts{'sarules'}{$type}}) { + + # only show top n lines; all when topn is "all" + if ($topn ne 'all' and $i > $topn) { + push @report, "...\n"; + last; + } + my $n = $Counts{'sarules'}{$type}{$_}; + my $nham = $Counts{'sarules'}{'Ham'}{$_}; + my $nspam = $Counts{'sarules'}{'Spam'}{$_}; + # rank, count, % msgs, % spam, % ham + push @report, sprintf "%4d %8d %6.2f%% %6.2f%% %6.2f%% %s\n", + $i++, + $n, + $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * $n / $Totals{'totalmsgs'}, + $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspam / $Totals{'totalspam'}, + $Totals{'totalham'} == 0 ? 0 : 100.0 * $nham / $Totals{'totalham'}, + $_; + my $len = length($report[-1]) - 1; + $maxlen = $len if ($len > $maxlen); + } + + if (scalar @report) { + print "\n", $sep1 x $maxlen, "\n"; + print "SpamAssassin Rule Hits: $type\n"; + print $sep2 x $maxlen, "\n"; + print "Rank Hits % Msgs % Spam % Ham Score Rule\n"; + print "---- ---- ------ ------ ----- ----- ----\n"; + print @report; + print $sep1 x $maxlen, "\n"; + } + } + + my ($def_limit_spam, $def_limit_ham) = split /[\s,]+/, $Defaults{'sarules'}; + my ($limit_spam, $limit_ham) = split /[\s,]+/, $Opts{'sarules'}; + $limit_spam = $def_limit_spam if $limit_spam eq ''; + $limit_ham = $def_limit_ham if $limit_ham eq ''; + + getSAHitsReport('Spam', $limit_spam); + getSAHitsReport('Ham', $limit_ham); +} + +# Autolearn report, only available if enabled in amavis $log_templ template +# +=pod + ====================================================================== + Autolearn Msgs Spam Ham % Msgs % Spam % Ham + ---------------------------------------------------------------------- + Spam 36 36 0 66.67% 76.60% 0.00% + Ham 2 0 2 3.70% 0.00% 28.57% + No 7 4 3 12.96% 8.51% 42.86% + Disabled 6 6 0 11.11% 12.77% 0.00% + Failed 2 1 1 3.70% 2.13% 14.29% + ---------------------------------------------------------------------- + Totals 53 47 6 98.15% 100.00% 85.71% + ====================================================================== +=cut +sub printAutolearnReport { + #print "printAutolearnReport:\n" if ($Opts{'debug'}); + return unless (keys %{$Counts{'autolearn'}}); + + our $maxlen = 0; + our ($nhamtotal, $nspamtotal); + + sub getAutolearnReport($) { + my ($type) = @_; + my @report = (); + + # SA 2.5/2.6 : ham/spam/no + # SA 3.0+ : ham/spam/no/disabled/failed/unavailable + for (qw(spam ham no disabled failed unavailable)) { + + next unless (exists $Counts{'autolearn'}{'Spam'}{$_} or exists $Counts{'autolearn'}{'Ham'}{$_}); + #print "printAutolearnReport: type: $_\n" if ($Opts{'debug'}); + + my $nham = exists $Counts{'autolearn'}{'Ham'}{$_} ? $Counts{'autolearn'}{'Ham'}{$_} : 0; + my $nspam = exists $Counts{'autolearn'}{'Spam'}{$_} ? $Counts{'autolearn'}{'Spam'}{$_} : 0; + my $nboth = $nham + $nspam; + $nhamtotal += $nham; $nspamtotal += $nspam; + # type, nspam, nham, % msgs, % spam, % ham + push @report, sprintf "%-13s %9d %9d %9d %6.2f%% %6.2f%% %6.2f%%\n", + ucfirst $_, + $nspam + $nham, + $nspam, + $nham, + $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * $nboth / $Totals{'totalmsgs'}, + $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspam / $Totals{'totalspam'}, + $Totals{'totalham'} == 0 ? 0 : 100.0 * $nham / $Totals{'totalham'}; + + my $len = length($report[-1]) - 1; + $maxlen = $len if ($len > $maxlen); + } + return @report; + } + + my @report_spam = getAutolearnReport('Spam'); + + if (scalar @report_spam) { + print "\n", $sep1 x $maxlen, "\n"; + print "Autolearn Msgs Spam Ham % Msgs % Spam % Ham\n"; + print $sep2 x $maxlen, "\n"; + print @report_spam; + print $sep2 x $maxlen, "\n"; + + printf "%-13s %9d %9d %9d %6.2f%% %6.2f%% %6.2f%%\n", + 'Totals', + $nspamtotal + $nhamtotal, + $nspamtotal, + $nhamtotal, + $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * ($nspamtotal + $nhamtotal) / $Totals{'totalmsgs'}, + $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspamtotal / $Totals{'totalspam'}, + $Totals{'totalham'} == 0 ? 0 : 100.0 * $nhamtotal / $Totals{'totalham'}; + print $sep1 x $maxlen, "\n"; + } +} + + +# Timings percentiles report, used for amavis message scanning and spamassassin timings +=pod + ======================================================================================================================== + Scan Timing Percentiles % Time Total (ms) 0% 5% 25% 50% 75% 95% 100% + ------------------------------------------------------------------------------------------------------------------------ + AV-scan-2 (3) 69.23% 7209.00 2392.00 2393.50 2399.50 2407.00 2408.50 2409.70 2410.00 + SA check (2) 19.74% 2056.00 942.00 950.60 985.00 1028.00 1071.00 1105.40 1114.00 + SMTP DATA (3) 5.49% 572.00 189.00 189.20 190.00 191.00 191.50 191.90 192.00 + AV-scan-1 (3) 0.82% 85.00 11.00 12.60 19.00 27.00 37.00 45.00 47.00 + ... + ------------------------------------------------------------------------------------------------------------------------ + Total 10413.00 2771.00 2867.10 3251.50 3732.00 3821.00 3892.20 3910.00 + ======================================================================================================================== + + ======================================================================================================================== + SA Timing Percentiles % Time Total (ms) 0% 5% 25% 50% 75% 95% 100% + ------------------------------------------------------------------------------------------------------------------------ + tests_pri_0 (1) 97.17% 5323.00 5323.00 5323.00 5323.00 5323.00 5323.00 5323.00 5323.00 + check_razor2 (1) 91.68% 5022.00 5022.00 5022.00 5022.00 5022.00 5022.00 5022.00 5022.00 + check_dcc (1) 3.50% 192.00 192.00 192.00 192.00 192.00 192.00 192.00 192.00 + learn (1) 0.66% 36.00 36.00 36.00 36.00 36.00 36.00 36.00 36.00 + tests_pri_-1000 (1) 0.46% 25.00 25.00 25.00 25.00 25.00 25.00 25.00 25.00 + ... + ------------------------------------------------------------------------------------------------------------------------ + Total 5478.00 5478.00 5478.00 5478.00 5478.00 5478.00 5478.00 5478.00 + ======================================================================================================================== +=cut +sub printTimingsReport($$$$) { + my ($title, $timingsref, $totalsref, $cutoff) = @_; + my @tkeys = keys %$timingsref; + return unless scalar @tkeys; + + my (@p, @sorted, %perkey_totals, @col_subtotals); + my ($pcnt,$max_pcnt,$max_rows,$time_total_actual,$time_total_hypo,$subtotal_pcnt); + my @percents = split /[\s,]+/, $Opts{'timings_percentiles'}; + my $header_footer = $sep1 x 50 . ($sep1 x 10) x @percents; + my $header_end = $sep2 x 50 . ($sep2 x 10) x @percents; + my $title_width = '-28'; + + print "\n$header_footer\n"; + printf "%${title_width}s %6s %13s" ." %8s%%" x @percents , $title, "% Time", "Total (ms)", @percents; + print "\n$header_end\n"; + + # Sum the total time for each timing key + foreach my $key (@tkeys) { + foreach my $timeval (@{$$timingsref{$key}}) { + $perkey_totals{$key} += $timeval; + } + } + + # Sum total time spent scanning + map {$time_total_actual += $_} @$totalsref; + + # cutoff value used to limit the number of rows of output + # positive cutoff is a percentage of cummulative time + # negative cutoff limits number of rows + if ($cutoff >= 0) { + $max_pcnt = $cutoff != 100 ? $cutoff : 150; # 150% avoids roundoff errors + } + else { + $max_rows = -$cutoff; + } + my $rows = 0; + # sort each timing key's values, required to compute the list of percentiles + for (sort { $perkey_totals{$b} <=> $perkey_totals{$a} } @tkeys) { + last if (($max_rows and $rows >= $max_rows) or ($max_pcnt and $subtotal_pcnt >= $max_pcnt)); + + $pcnt = ($perkey_totals{$_} / $time_total_actual) * 100, + @sorted = sort { $a <=> $b } @{$$timingsref{$_}}; + @p = get_percentiles (@sorted, @percents); + + $subtotal_pcnt += $pcnt; + printf "%${title_width}s %6.2f%% %13.2f" . " %9.2f" x scalar (@p) . "\n", + $_ . ' (' . scalar(@{$$timingsref{$_}}) . ')', # key ( number of elements ) + $pcnt, # percent of total time + #$perkey_totals{$_} / 1000, # total time for this test + $perkey_totals{$_}, # total time for this test + #map {$_ / 1000} @p; # list of percentiles + @p; # list of percentiles + $rows++; + } + print "...\n" if ($rows != scalar @tkeys); + + print "$header_end\n"; + # actual total time as reported by amavis + @sorted = sort { $a <=> $b } @$totalsref; + @p = get_percentiles (@sorted, @percents); + printf "%${title_width}s %13.2f" . " %9.2f" x scalar (@p) . "\n", + 'Total', + #$time_total_actual / 1000, + $time_total_actual, + #map {$_ / 1000} @p; + @p; + + print "$header_footer\n"; +} + +# Most recent startup info report +# +sub printStartupInfoReport { + + return unless (keys %StartInfo); + + sub print2col($ $) { + my ($label,$val) = @_; + printf "%-50s %s\n", $label, $val; + } + + print "\nAmavis Startup\n"; + + print2col (" Amavis", $StartInfo{'ampath'}) if (exists $StartInfo{'ampath'}); + print2col (" Version", $StartInfo{'amversion'}) if (exists $StartInfo{'amversion'}); + print2col (" PID", $StartInfo{'Server'}{'pid'}) if (exists $StartInfo{'Server'}{'pid'}); + print2col (" Socket", $StartInfo{'Server'}{'socket'}) if (exists $StartInfo{'Server'}{'socket'}); + print2col (" TCP port", $StartInfo{'Server'}{'ip'}) if (exists $StartInfo{'Server'}{'ip'}); + print2col (" UID", $StartInfo{'Server'}{'uid'}) if (exists $StartInfo{'Server'}{'uid'}); + print2col (" GID", $StartInfo{'Server'}{'gid'}) if (exists $StartInfo{'Server'}{'gid'}); + print2col (" Logging", $StartInfo{'Logging'}) if (exists $StartInfo{'Logging'}); + print2col (" Configuration Files", $StartInfo{'Configs'}) if (exists $StartInfo{'Configs'}); + print2col (" SpamAssassin", $StartInfo{'sa_version'}) if (exists $StartInfo{'sa_version'}); + print2col (" SpamAssassin Debug Facilities", $StartInfo{'sa_debug'}) if (exists $StartInfo{'sa_debug'}); + print2col (" Database", $StartInfo{'db'}) if (exists $StartInfo{'db'}); + #if (keys %{$StartInfo{'IDs'}}) { + # print " Process startup user/group:\n"; + # print " User: $StartInfo{'IDs'}{'user'}, EUID: $StartInfo{'IDs'}{'euid'}, UID: $StartInfo{'IDs'}{'uid'}\n"; + # print " Group: $StartInfo{'IDs'}{'group'}, EGID: $StartInfo{'IDs'}{'egid'}, GID: $StartInfo{'IDs'}{'gid'}\n"; + #} + + sub print_modules ($ $) { + my ($key, $label) = @_; + print " $label\n"; + foreach (sort keys %{$StartInfo{$key}}) { + print " $_\n"; + foreach my $module (sort keys %{$StartInfo{$key}{$_}}) { + if ($StartInfo{$key}{$_}{$module}) { + print2col (" " . $module, $StartInfo{$key}{$_}{$module}); + } + else { + print2col (" " . $module, ""); + } + } + } + }; + print_modules('AVScanner', 'Antivirus scanners'); + print_modules('Code', 'Code, modules and external programs'); + print_modules('Decoders', 'Decoders'); + print_modules('SAPlugins', 'SpamAssassin plugins'); +} + +# Initialize the Getopts option list. Requires the Section table to +# be built already. +# +sub init_getopts_table() { + print "init_getopts_table: enter\n" if $Opts{'debug'} & D_ARGS; + + init_getopts_table_common(@supplemental_reports); + + add_option ('first_recip_only!'); + add_option ('show_first_recip_only=i', sub { $Opts{'first_recip_only'} = $_[1]; 1;}); + add_option ('startinfo!'); + add_option ('show_startinfo=i', sub { $Opts{'startinfo'} = $_[1]; 1; }); + add_option ('by_ccat_summary!'); + add_option ('show_by_ccat_summary=i', sub { $Opts{'by_ccat_summary'} = $_[1]; 1; }); + add_option ('noscore_percentiles', \&triway_opts); + add_option ('score_percentiles=s', \&triway_opts); + add_option ('noscore_frequencies', \&triway_opts); + add_option ('score_frequencies=s', \&triway_opts); + add_option ('nosa_timings', sub { $Opts{'sa_timings'} = 0; 1; }); + add_option ('sa_timings=i'); + add_option ('sa_timings_percentiles=s'); + add_option ('notimings', sub { $Opts{'timings'} = 0; 1; }); + add_option ('timings=i'); + add_option ('timings_percentiles=s'); + add_option ('nosarules', \&triway_opts); + add_option ('sarules=s', \&triway_opts); + #add_option ('nop0f', \&triway_opts); + #add_option ('p0f=s', \&triway_opts); + add_option ('autolearn!'); + add_option ('show_autolearn=i', sub { $Opts{'autolearn'} = $_[1]; 1; }); +} + +# Builds the entire @Section table used for data collection +# +# Each Section entry has as many as six fields: +# +# 1. Section array reference +# 2. Key to %Counts, %Totals accumulator hashes, and %Collecting hash +# 3. Output in Detail report? (must also a %Counts accumulator) +# 4. Numeric output format specifier for Summary report +# 5. Section title for Summary and Detail reports +# 6. A hash to a divisor used to calculate the percentage of a total for that key +# +# Use begin_section_group/end_section_group to create groupings around sections. +# +# Sections can be freely reordered if desired, but maintain proper group nesting. +# +sub build_sect_table() { + print "build_sect_table: enter\n" if $Opts{'debug'} & D_SECT; + my $S = \@Sections; + + # References to these are used in the Sections table below; we'll predeclare them. + $Totals{'totalmsgs'} = 0; + + # Place configuration and critical errors first + + # SECTIONREF, NAME, DETAIL, FMT, TITLE, DIVISOR + begin_section_group ($S, 'warnings'); + add_section ($S, 'fatal', 1, 'd', '*Fatal'); + add_section ($S, 'panic', 1, 'd', '*Panic'); + add_section ($S, 'warningsecurity', 1, 'd', '*Warning: Security risk'); + add_section ($S, 'avtimeout', 1, 'd', '*Warning: Virus scanner timeout'); + add_section ($S, 'avconnectfailure', 1, 'd', '*Warning: Virus scanner connection failure'); + add_section ($S, 'warningsmtpshutdown', 1, 'd', '*Warning: SMTP shutdown'); + add_section ($S, 'warningsql', 1, 'd', '*Warning: SQL problem'); + add_section ($S, 'warningaddressmodified', 1, 'd', '*Warning: Email address modified'); + add_section ($S, 'warningnoquarantineid', 1, 'd', '*Warning: Message missing X-Quarantine-ID header'); + add_section ($S, 'warning', 1, 'd', 'Miscellaneous warnings'); + end_section_group ($S, 'warnings'); + + begin_section_group ($S, 'scanned', "\n"); + add_section ($S, 'totalmsgs', 0, 'd', [ 'Total messages scanned', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'bytesscanned', 0, 'Z', 'Total bytes scanned'); # Z means print scaled as in 1k, 1m, etc. + end_section_group ($S, 'scanned', $sep1); + + # Blocked / Passed + # Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN + begin_section_group ($S, 'passblock', "\n"); + begin_section_group ($S, 'blocked', "\n"); + add_section ($S, 'totalblocked', 0, 'd', [ 'Blocked', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'malwareblocked', 1, 'd', ' Malware blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'bannednameblocked', 1, 'd', ' Banned name blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'uncheckedblocked', 1, 'd', ' Unchecked blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'spamblocked', 1, 'd', ' Spam blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'spamdiscarded', 0, 'd', ' Spam discarded (no quarantine)', \$Totals{'totalmsgs'}); + add_section ($S, 'spammyblocked', 1, 'd', ' Spammy blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'badheaderblocked', 1, 'd', ' Bad header blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'oversizedblocked', 1, 'd', ' Oversized blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'mtablocked', 1, 'd', ' MTA blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'cleanblocked', 1, 'd', ' Clean blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'tempfailblocked', 1, 'd', ' Tempfail blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'otherblocked', 1, 'd', ' Other blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'blocked'); + + begin_section_group ($S, 'passed', "\n"); + add_section ($S, 'totalpassed', 0, 'd', [ 'Passed', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'malwarepassed', 1, 'd', ' Malware passed', \$Totals{'totalmsgs'}); + add_section ($S, 'bannednamepassed', 1, 'd', ' Banned name passed', \$Totals{'totalmsgs'}); + add_section ($S, 'uncheckedpassed', 1, 'd', ' Unchecked passed', \$Totals{'totalmsgs'}); + add_section ($S, 'spampassed', 1, 'd', ' Spam passed', \$Totals{'totalmsgs'}); + add_section ($S, 'spammypassed', 1, 'd', ' Spammy passed', \$Totals{'totalmsgs'}); + add_section ($S, 'badheaderpassed', 1, 'd', ' Bad header passed', \$Totals{'totalmsgs'}); + add_section ($S, 'oversizedpassed', 1, 'd', ' Oversized passed', \$Totals{'totalmsgs'}); + add_section ($S, 'mtapassed', 1, 'd', ' MTA passed', \$Totals{'totalmsgs'}); + add_section ($S, 'cleanpassed', 1, 'd', ' Clean passed', \$Totals{'totalmsgs'}); + add_section ($S, 'tempfailpassed', 1, 'd', ' Tempfail passed', \$Totals{'totalmsgs'}); + add_section ($S, 'otherpassed', 1, 'd', ' Other passed', \$Totals{'totalmsgs'}); + end_section_group ($S, 'passed'); + end_section_group ($S, 'passblock', $sep1); + + if ($Opts{'by_ccat_summary'}) { + # begin level 1 group + begin_section_group ($S, 'by_ccat', "\n"); + + # begin level 2 groupings + begin_section_group ($S, 'malware', "\n"); # level 2 + add_section ($S, 'totalmalware', 0, 'd', [ 'Malware', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'malwarepassed', 0, 'd', ' Malware passed', \$Totals{'totalmsgs'}); + add_section ($S, 'malwareblocked', 0, 'd', ' Malware blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'malware'); + + begin_section_group ($S, 'banned', "\n"); + add_section ($S, 'totalbanned', 0, 'd', [ 'Banned', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'bannednamepassed', 0, 'd', ' Banned file passed', \$Totals{'totalmsgs'}); + add_section ($S, 'bannednameblocked', 0, 'd', ' Banned file blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'banned'); + + begin_section_group ($S, 'unchecked', "\n"); + add_section ($S, 'totalunchecked', 0, 'd', [ 'Unchecked', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'uncheckedpassed', 0, 'd', ' Unchecked passed', \$Totals{'totalmsgs'}); + add_section ($S, 'uncheckedblocked', 0, 'd', ' Unchecked blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'unchecked'); + + begin_section_group ($S, 'spam', "\n"); + add_section ($S, 'totalspam', 0, 'd', [ 'Spam', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'spammypassed', 0, 'd', ' Spammy passed', \$Totals{'totalmsgs'}); + add_section ($S, 'spammyblocked', 0, 'd', ' Spammy blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'spampassed', 0, 'd', ' Spam passed', \$Totals{'totalmsgs'}); + add_section ($S, 'spamblocked', 0, 'd', ' Spam blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'spamdiscarded', 0, 'd', ' Spam discarded (no quarantine)', \$Totals{'totalmsgs'}); + end_section_group ($S, 'spam'); + + begin_section_group ($S, 'ham', "\n"); + add_section ($S, 'totalham', 0, 'd', [ 'Ham', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'badheaderpassed', 0, 'd', ' Bad header passed', \$Totals{'totalmsgs'}); + add_section ($S, 'badheaderblocked', 0, 'd', ' Bad header blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'oversizedpassed', 0, 'd', ' Oversized passed', \$Totals{'totalmsgs'}); + add_section ($S, 'oversizedblocked', 0, 'd', ' Oversized blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'mtapassed', 0, 'd', ' MTA passed', \$Totals{'totalmsgs'}); + add_section ($S, 'mtablocked', 0, 'd', ' MTA blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'cleanpassed', 0, 'd', ' Clean passed', \$Totals{'totalmsgs'}); + add_section ($S, 'cleanblocked', 0, 'd', ' Clean blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'ham'); + + begin_section_group ($S, 'other', "\n"); + add_section ($S, 'totalother', 0, 'd', [ 'Other', '-' ], \$Totals{'totalmsgs'}); + add_section ($S, 'tempfailpassed', 0, 'd', ' Tempfail passed', \$Totals{'totalmsgs'}); + add_section ($S, 'tempfailblocked', 0, 'd', ' Tempfail blocked', \$Totals{'totalmsgs'}); + add_section ($S, 'otherpassed', 0, 'd', ' Other passed', \$Totals{'totalmsgs'}); + add_section ($S, 'otherblocked', 0, 'd', ' Other blocked', \$Totals{'totalmsgs'}); + end_section_group ($S, 'other'); + # end level 2 groupings + + # end level 1 group + end_section_group ($S, 'by_ccat', $sep1); + } + + begin_section_group ($S, 'misc', "\n"); + add_section ($S, 'virusscanskipped', 1, 'd', 'Virus scan skipped'); + add_section ($S, 'sabypassed', 0, 'd', 'SpamAssassin bypassed'); + add_section ($S, 'satimeout', 0, 'd', 'SpamAssassin timeout'); + add_section ($S, 'released', 1, 'd', 'Released from quarantine'); + add_section ($S, 'defanged', 1, 'd', 'Defanged'); + add_section ($S, 'truncatedheader', 0, 'd', 'Truncated headers > 998 characters'); + add_section ($S, 'truncatedmsg', 0, 'd', 'Truncated message passed to SpamAssassin'); + add_section ($S, 'tagged', 0, 'd', 'Spam tagged'); + add_section ($S, 'smtpresponse', 1, 'd', 'SMTP response'); + add_section ($S, 'badaddress', 1, 'd', 'Bad address syntax'); + add_section ($S, 'fakesender', 1, 'd', 'Fake sender'); + add_section ($S, 'archiveextract', 1, 'd', 'Archive extraction problem'); + add_section ($S, 'dsnsuppressed', 1, 'd', 'DSN suppressed'); + add_section ($S, 'dsnnotification', 1, 'd', 'DSN notification (debug supplemental)'); + add_section ($S, 'bouncekilled', 1, 'd', 'Bounce killed'); + add_section ($S, 'bouncerescued', 1, 'd', 'Bounce rescued'); + add_section ($S, 'bounceunverifiable', 1, 'd', 'Bounce unverifiable'); + add_section ($S, 'nosubject', 0, 'd', 'Subject header inserted'); + add_section ($S, 'whitelisted', 1, 'd', 'Whitelisted'); + add_section ($S, 'blacklisted', 1, 'd', 'Blacklisted'); + add_section ($S, 'penpalsaved', 1, 'd', 'Penpals saved from kill'); + add_section ($S, 'tmppreserved', 1, 'd', 'Preserved temporary directory'); + add_section ($S, 'dccerror', 1, 'd', 'DCC error'); + add_section ($S, 'mimeerror', 1, 'd', 'MIME error'); + add_section ($S, 'defangerror', 1, 'd', 'Defang error'); + add_section ($S, 'badheadersupp', 1, 'd', 'Bad header (debug supplemental)'); + add_section ($S, 'fileoutputskipped', 0, 'd', 'File(1) output skipped'); + add_section ($S, 'localdeliveryskipped', 1, 'd', 'Local delivery skipped'); + add_section ($S, 'extramodules', 1, 'd', 'Extra code modules loaded at runtime'); + add_section ($S, 'malwarebyscanner', 1, 'd', 'Malware by scanner'); + add_section ($S, 'malwaretospam', 1, 'd', 'Malware to spam conversion'); + add_section ($S, 'contenttype', 1, 'd', 'Content types'); + add_section ($S, 'bayes', 1, 'd', 'Bayes probability'); + add_section ($S, 'p0f', 1, 'd', 'p0f fingerprint'); + add_section ($S, 'sadiags', 1, 'd', 'SpamAssassin diagnostics'); + end_section_group ($S, 'misc'); + + print "build_sect_table: exit\n" if $Opts{'debug'} & D_SECT; +} + +# XXX create array of defaults for detail <5, 5-9, >10 +sub init_defaults() { + map { $Opts{$_} = $Defaults{$_} unless exists $Opts{$_} } keys %Defaults; + if (! $Opts{'standalone'}) { + # LOGWATCH these take affect if no env present (eg. nothing in conf file) + # 0 to 4 nostartinfo, notimings, nosarules, score_frequencies=0, score_percentiles=0, noautolearn + # 5 to 9 nostartinfo, timings=95, sarules = 20 20, score_frequencies=defaults, score_percentiles=defaults, autolearn + # 10 + startinfo, timings=100, sarules = all all score_frequencies=defaults, score_percentiles=defaults, autolearn + + if ($Opts{'detail'} < 5) { # detail 0 to 4, disable all supplimental reports + $Opts{'autolearn'} = 0; + #$Opts{'p0f'} = 0; + $Opts{'timings'} = 0; + $Opts{'sa_timings'} = 0; + $Opts{'sarules'} = 0; + $Opts{'startinfo'} = 0; + $Opts{'score_frequencies'} = ''; + $Opts{'score_percentiles'} = ''; + } + elsif ($Opts{'detail'} < 10) { # detail 5 to 9, disable startinfo report + $Opts{'startinfo'} = 0; + } + else { # detail 10 and up, full reports + #$Opts{'p0f'} = 'all all'; + $Opts{'timings'} = 100; + $Opts{'sa_timings'} = 100; + $Opts{'sarules'} = 'all all'; + } + } +} + +# Return a usage string, built from: +# arg1 + +# $usage_str + +# a string built from each usable entry in the @Sections table. +# +sub usage($) { + my $ret = ""; + $ret = "@_\n" if ($_[0]); + $ret .= $usage_str; + my ($name, $desc); + foreach my $sect (get_usable_sectvars(@Sections, 0)) { + $name = lc $sect->{NAME}; + $desc = $sect->{TITLE}; + $ret .= sprintf " --%-38s%s\n", "$name" . ' LEVEL', "$desc"; + } + $ret .= "\n"; + return $ret; +} + +sub strip_trace($) { + # at (eval 37) line 306, line 4. + # at /usr/sbin/amavisd-maia line 2895, line 22. + #$_[0] =~ s/ at \(.+\) line \d+(?:, \ line \d+)?\.$//; + #$_[0] =~ s/ at (\S+) line \d+(?:, \ line \d+)?\.$/: $1/; + while ($_[0] =~ s/ at (?:\(eval \d+\)|\S+) line \d+(?:, \ line \d+)?\.//) { + ; + } + #print "strip_trace: \"$_[0]\"\n"; + return $_[0]; +} + +# Getopt helper, sets an option in Opts hash to one of three +# values: its default, the specified value, or 0 if the option +# was the "no" prefixed variant. +# +sub triway_opts ($ $) { + my ($opt,$val) = @_; + + print "triway_opts: OPT: $opt, VAL: $val\n" if $Opts{'debug'} & D_ARGS; + die "Option \"--${opt}\" requires an argument" if ($val =~ /^--/); + + if ($opt =~ s/^no//i) { + $Opts{$opt} = 0; + } elsif ('default' =~ /^${val}$/i) { + $Opts{$opt} = $Defaults{$opt}; + } + else { + $Opts{$opt} = $val; + } +} + +exit(0); + +# vi: shiftwidth=3 tabstop=3 syntax=perl et diff --git a/amavis-logwatch.1 b/amavis-logwatch.1 new file mode 100644 index 0000000..d8488a4 --- /dev/null +++ b/amavis-logwatch.1 @@ -0,0 +1,923 @@ +.TH AMAVIS-LOGWATCH 1 +.ad +.fi +.SH NAME +amavis-logwatch +\- +An Amavisd-new log parser and analysis utility +.SH "SYNOPSIS" +.na +.nf +.fi +\fBamavis-logwatch\fR [\fIoptions\fR] [\fIlogfile ...\fR] +.SH DESCRIPTION +.ad +.fi +The \fBamavis-logwatch\fR(1) utility is an Amavisd-new log parser +that produces summaries, details, and statistics regarding +the operation of Amavisd-new (henceforth, simply called Amavis). +.PP +This utility can be used as a +standalone program, or as a Logwatch filter module to produce +Amavisd-new summary and detailed reports from within Logwatch. +.PP +\fBAmavis-logwatch\fR is able to produce +a wide range of reports with data grouped and sorted as much as possible +to reduce noise and highlight patterns. +Brief summary reports provide a +quick overview of general Amavis operations and message +delivery, calling out warnings that may require attention. +Detailed reports provide easy to scan, hierarchically-arranged +and organized information, with as much or little detail as +desired. +.PP +Much of the interesting data is available when Amavis' +$log_level is set to at least 2. +See \fBAmavis Log Level\fR below. +.PP +\fBAmavis-logwatch\fR outputs two principal sections: a \fBSummary\fR section +and a \fBDetailed\fR section. +For readability and quick scanning, all event or hit counts appear in the left column, +followed by brief description of the event type, and finally additional +statistics or count representations may appear in the rightmost column. + +The following segment from a sample Summary report illustrates: +.RS 4 +.nf + +****** Summary ******************************************** + + 9 Miscellaneous warnings + + 20313 Total messages scanned ---------------- 100.00% +1008.534M Total bytes scanned 1,057,524,252 +======== ================================================ + + 1190 Blocked ------------------------------- 5.86% + 18 Malware blocked 0.09% + 4 Banned name blocked 0.02% + 416 Spam blocked 2.05% + 752 Spam discarded (no quarantine) 3.70% + + 19123 Passed -------------------------------- 94.14% + 47 Bad header passed 0.23% + 19076 Clean passed 93.91% +======== ================================================ + + 18 Malware ------------------------------- 0.09% + 18 Malware blocked 0.09% + + 4 Banned -------------------------------- 0.02% + 4 Banned file blocked 0.02% + + 1168 Spam ---------------------------------- 5.75% + 416 Spam blocked 2.05% + 752 Spam discarded (no quarantine) 3.70% + + 19123 Ham ----------------------------------- 94.14% + 47 Bad header passed 0.23% + 19076 Clean passed 93.91% +======== ================================================ + + 1982 SpamAssassin bypassed + 32 Released from quarantine + 2 DSN notification (debug supplemental) + 2 Bounce unverifiable + 2369 Whitelisted + 2 Blacklisted + 12 MIME error + 58 Bad header (debug supplemental) + 40 Extra code modules loaded at runtime + +.fi +.RE 0 +The report indicates there were 9 general warnings, and +\fBAmavis\fR scanned a total of 20313 messages +for a total of 1008.53 megabytes or 1,057,524,252 bytes. +The next summary groups shows the Blocked / Passed overview, +with 1190 Blocked messages (broken down as 18 messages blocked as malware, +4 messages with banned names, 416 spam messages, and 752 discarded +messages), and 19123 Passed messages (47 messages with bad headers +and 19076 clean messages). + +The next (optional) summary grouping shows message disposition by contents category. +There were 18 malware messages and 4 banned file messages (all blocked), +1168 Spam messages, of which 416 were blocked (quarantined) and 752 discarded. +Finally, there were 19123 messages consdidered to be Ham (i.e. not spam), 47 +of which contained bad headers. + +Additional count summaries for a variety of events are also listed. +.PP +There are dozens of sub-sections available in the \fBDetailed\fR report, each of +whose output can be controlled in various ways. +Each sub-section attempts to group and present the most meaningful data at superior levels, +while pushing less useful or \fInoisy\fR data towards inferior levels. +The goal is to provide as much benefit as possible from smart grouping of +data, to allow faster report scanning, pattern identification, and problem solving. +Data is always sorted in descending order by count, and then numerically by IP address +or alphabetically as appropriate. +.PP +The following Spam blocked segment from a sample \fBDetailed\fR report +illustrates the basic hierarchical level structure of \fBamavis-logwatch\fR: +.RS 4 +.nf + +****** Detailed ******************************************* + + 19346 Spam blocked ----------------------------------- + 756 from@example.com + 12 10.0.0.2 + 12 <> + 12 192.168.2.2 + 12 <> + 5 192.168.2.1 + ... + +.fi +.RE 0 +.PP +The \fBamavis-logwatch\fR utility reads from STDIN or from the named Amavis +\fIlogfile\fR. +Multiple \fIlogfile\fR arguments may be specified, each processed +in order. +The user running \fBamavis-logwatch\fR must have read permission on +each named log file. +.PP +.SS Options +The options listed below affect the operation of \fBamavis-logwatch\fR. +Options specified later on the command line override earlier ones. +Any option may be abbreviated to an unambiguous length. + +.IP "\fB--[no]autolearn\fR" +.PD 0 +.IP "\fB--show_autolearn \fIboolean\fR" +.PD +Enables (disables) output of the autolearn report. +This report is only available if the default Amavis \fB$log_templ\fR +has been modified to provide autolearn results in log entries. +This can be done by uncommenting two lines in the Amavis program itself (where the +default log templates reside), or by correctly adding the \fB$log_templ\fR +variable to the \fBamavisd.conf\fR file. +See Amavis' \fBREADME.customize\fR and search near the end +of the Amavisd program for "autolearn". +.IP "\fB--[no]by_ccat_summary\fR" +.PD 0 +.IP "\fB--show_by_ccat_summary \fIboolean\fR" +.PD +Enables (disables) the by contents category summary in the \fBSummary\fR section. +Default: enabled. +.IP "\fB-f \fIconfig_file\fR" +.PD 0 +.IP "\fB--config_file \fIconfig_file\fR" +.PD +Use an alternate configuration file \fIconfig_file\fR instead of +the default. +This option may be used more than once. +Multiple configuration files will be processed in the order presented on the command line. +See \fBCONFIGURATION FILE\fR below. +.IP "\fB--debug \fIkeywords\fR" +Output debug information during the operation of \fBamavis-logwatch\fR. +The parameter \fIkeywords\fR is one or more comma or space separated keywords. +To obtain the list of valid keywords, use --debug xxx where xxx is any invalid keyword. +.IP "\fB--detail \fIlevel\fR" +Sets the maximum detail level for \fBamavis-logwatch\fR to \fIlevel\fR. +This option is global, overriding any other output limiters described below. + +The \fBamavis-logwatch\fR utility +produces a \fBSummary\fR section, a \fBDetailed\fR section, and +additional report sections. +With \fIlevel\fR less than 5, \fBamavis-logwatch\fR will produce +only the \fBSummary\fR section. +At \fIlevel\fR 5 and above, the \fBDetailed\fR section, and any +additional report sections are candidates for output. +Each incremental increase in \fIlevel\fR generates one additional +hierarchical sub-level of output in the \fBDetailed\fR section of the report. +At \fIlevel\fR 10, all levels are output. +Lines that exceed the maximum report width (specified with +\fBmax_report_width\fR) will be cut. +Setting \fIlevel\fR to 11 will prevent lines in the report from being cut (see also \fB--line_style\fR). +.IP "\fB--[no]first_recip_only\fR" +.PD 0 +.IP "\fB--show_first_recip_only \fIboolean\fR" +.PD +Specifies whether or not to sort by, and show, only the first +recipient when a scanned messages contains multiple recipients. +.IP "\fB--help\fR" +Print usage information and a brief description about command line options. +.IP "\fB--ipaddr_width \fIwidth\fR" +Specifies that IP addresses in address/hostname pairs should be printed +with a field width of \fIwidth\fR characters. +Increasing the default may be useful for systems using long IPv6 addresses. +.IP "\fB-l limiter=levelspec\fR" +.PD 0 +.IP "\fB--limit limiter=levelspec\fR" +.PD +Sets the level limiter \fIlimiter\fR with the specification \fIlevelspec\fR. +.IP "\fB--line_style \fIstyle\fR" +Specifies how to handle long report lines. +Three styles are available: \fBfull\fR, \fBtruncate\fR, and \fBwrap\fR. +Setting \fIstyle\fR to \fBfull\fR will prevent cutting lines to \fBmax_report_width\fR; +this is what occurs when \fBdetail\fR is 11 or higher. +When \fIstyle\fR is \fBtruncate\fR (the default), +long lines will be truncated according to \fBmax_report_width\fR. +Setting \fIstyle\fR to \fBwrap\fR will wrap lines longer than \fBmax_report_width\fR such that +left column hit counts are not obscured. +This option takes precedence over the line style implied by the \fBdetail\fR level. +The options \fB--full\fR, \fB--truncate\fR, and \fB--wrap\fR are synonyms. + +.IP "\fB--nodetail\fR" +Disables the \fBDetailed\fR section of the report, and all supplemental reports. +This option provides a convenient mechanism to quickly disable all sections +under the \fBDetailed\fR report, where subsequent command line +options may re-enable one or more sections to create specific reports. + +.PD 0 +.IP "\fB--sarules \fR\`\fIS,H\fR\'" +.IP "\fB--sarules default" +.PD +Enables the SpamAssassin Rules Hit report. +The comma-separated \fIS\fR and \fIH\fR arguments are top N values for the Spam and Ham +reports, respectively, and can be any integer greater than or equal to 0, or the keyword \fBall\fR. +The keyword \fBdefault\fR uses the built-in default values. +.IP "\fB--nosarules\fR" +Disables the SpamAssassin Rules Hit report. + +.PD 0 +.IP "\fB--sa_timings \fR\fInrows\fR" +Enables the SpamAssassin Timings percentiles report. +The report can be limited to the top N rows with the \fInrows\fR argument. +This report requires Amavis 2.6+ and SpamAssassin 3.3+. +.PD +.IP "\fB--sa_timings_percentiles \fR\`\fIP1 [P2 ...]\fR\'" +Specifies the percentiles shown in the SpamAssassin Timings report. +The arguments \fIP1 ...\fR are integers from 0 to 100 inclusive. +Their order will be preserved in the report. +.IP "\fB--nosa_timings\fR" +Disables the SpamAssassin Timings report. +.IP "\fB--version\fR" +Print \fBamavis-logwatch\fR version information. + +.PD 0 +.IP "\fB--score_frequencies \fR\`\fIB1 [B2 ...]\fR\'" +.IP "\fB--score_frequencies default" +.PD +Enables the Spam Score Frequency report. +The arguments \fIB1 ...\fR are frequency distribution buckets, and can be any real numbers. +Their order will be preserved in the report. +The keyword \fBdefault\fR uses the built-in default values. +.IP "\fB--noscore_frequencies\fR" +Disables the Spam Score Frequency report. + +.PD 0 +.IP "\fB--score_percentiles \fR\`\fIP1 [P2 ...]\fR\'" +.IP "\fB--score_percentiles default" +.PD +Enables the Spam Score Percentiles report. +The arguments \fIP1 ...\fR specify the percentiles shown in the report, +and are integers from 0 to 100 inclusive. +The keyword \fBdefault\fR uses the built-in default values. +.IP "\fB--noscore_percentiles\fR" +Disables the Spam Score Percentiles report. + +.IP "\fB--[no]sect_vars\fR" +.PD 0 +.IP "\fB--show_sect_vars \fIboolean\fR" +.PD +Enables (disables) supplementing each \fBDetailed\fR section title +with the name of that section's level limiter. +The name displayed is the command line option (or configuration +file variable) used to limit that section's output. +. +With the large number of level limiters available in \fBamavis-logwatch\fR, +this a convenient mechanism for determining exactly which level limiter +affects a section. +.IP "\fB--[no]startinfo\fR" +.PD 0 +.IP "\fB--show_startinfo \fIboolean\fR" +.PD +Enables (disables) the Amavis startup report showing most recent Amavis startup details. +.IP "\fB--[no]summary\fR" +.IP "\fB--show_summary\fR" +Enables (disables) displaying of the the \fBSummary\fR section of the report. +The variable Amavis_Show_Summary in used in a configuration file. +.IP "\fB--syslog_name \fInamepat\fR" +Specifies the syslog service name that \fBamavis-logwatch\fR uses +to match syslog lines. +Only log lines whose service name matches +the perl regular expression \fInamepat\fR will be used by +\fBamavis-logwatch\fR; all non-matching lines are silently ignored. +This is useful when a pre-installed Amavis package uses a name +other than the default (\fBamavis\fR). + +\fBNote:\fR if you use parenthesis in your regular expression, be sure they are cloistering +and not capturing: use \fB(?:\fIpattern\fB)\fR instead of \fB(\fIpattern\fB)\fR. + +.PD 0 +.IP "\fB--timings \fR\fIpercent\fR" +Enables the Amavis Scan Timings percentiles report. +The report can be top N-percent limited with the \fIpercent\fR argument. +.PD +.IP "\fB--timings_percentiles \fR\`\fIP1 [P2 ...]\fR\'" +Specifies the percentiles shown in the Scan Timings report. +The arguments \fIP1 ...\fR are integers from 0 to 100 inclusive. +Their order will be preserved in the report. +.IP "\fB--notimings\fR" +Disables the Amavis Scan Timings report. +.IP "\fB--version\fR" +Print \fBamavis-logwatch\fR version information. + +.SS Level Limiters +.PP +The output of every section in the \fBDetailed\fR report is controlled by a level limiter. +The name of the level limiter variable will be output when the \fBsect_vars\fR option is set. +Level limiters are set either via command line in standalone mode with \fB--limit \fIlimiter\fB=\fIlevelspec\fR option, +or via configuration file variable \fB$amavis_\fIlimiter\fB=\fIlevelspec\fR. +Each limiter requires a \fIlevelspec\fR argument, which is described below in \fBLEVEL CONTROL\fR. + +The list of level limiters is shown below. + +.de TQ +. br +. ns +. TP \\$1 +.. + +.PD 0 +.PP +Amavis major contents category (ccatmajor) sections, listed in order of priority: +VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH, OVERSIZED, MTA, CLEAN. + +.IP "\fBMalwareBlocked" +.IP "\fBMalwarePassed" +Blocked or passed messages that contain malware (ccatmajor: VIRUS). + +.IP "\fBBannedNameBlocked" +.IP "\fBBannedNamePassed" +Blocked or passed messages that contain banned names in MIME parts (ccatmajor: BANNED). + +.IP "\fBUncheckedBlocked" +.IP "\fBUncheckedPassed" +Blocked or passed messages that were not checked by a virus scanner or SpamAssassin (Amavis ccatmajor: UNCHECKED). + +.IP "\fBSpamBlocked" +.IP "\fBSpamPassed" +Blocked or passed messages that were considered spam that reached kill level (Amavis ccatmajor: SPAM) + +.IP "\fBSpammyBlocked" +.IP "\fBSpammyPassed" +Blocked or passed messages that were considered spam, but did not reach kill level (Amavis ccatmajor: SPAMMY) + +.IP "\fBBadHeaderBlocked" +.IP "\fBBadHeaderPassed" +Blocked or passed messages that contain bad mail headers (ccatmajor: BAD-HEADER). + +.IP "\fBOversizedBlocked" +.IP "\fBOversizedPassed" +Blocked or passed messages that were considered oversized (Amavis ccatmajor: OVERSIZED). + +.IP "\fBMtaBlocked" +.IP "\fBMtaPassed" +Blocked or passed messages due to failure to re-inject to MTA (Amavis ccatmajor: MTA-BLOCKED). +Occurrences of this event indicates a configuration problem. +[ note: I don't believe mtapassed occurs, but exists for completeness.] + +.IP "\fBOtherBlocked" +.IP "\fBOtherPassed" +Blocked or passed messages that are not any of other major contents categories (Amavis ccatmajor: OTHER). + + +.IP "\fBTempFailBlocked" +.IP "\fBTempfailPassed" +Blocked or passed messages that had a temporary failure (Amavis ccatmajor: TEMPFAIL) + +.IP "\fBCleanBlocked" +.IP "\fBCleanPassed " +Messages blocked or passed which were considered clean (Amavis ccatmajor: CLEAN; i.e. non-spam, non-viral). + +.PP +Other sections, arranged alphabetically: + +.IP "\fBAvConnectFailure" +Problems connecting to Anti-Virus scanner(s). + +.IP "\fBAvTimeout" +Timeouts awaiting responses from Anti-Virus scanner(s). + +.IP "\fBArchiveExtract" +Archive extraction problems. + +.IP "\fBBadHeaderSupp" +Supplemental debug information regarding messages containing bad mail headers. + +.IP "\fBBayes" +Messages frequencies by Bayesian probability buckets. + +.IP "\fBBadAddress" +Invalid mail address syntax. + +.IP "\fBBlacklisted" +Messages that were (soft-)blacklisted. See also Whitelisted below. + +.IP "\fBBounceKilled" +.IP "\fBBounceRescued" +.IP "\fBBounceUnverifiable" +Disposition of incoming bounce messages (DSNs). + +.IP "\fBContentType" +MIME attachment breakdown by type/subtype. + +.IP "\fBDccError" +Errors encountered with or returned by DCC. + +.IP "\fBDefangError" +Errors encountered during defang process. + +.IP "\fBDefanged" +Messages defanged (rendered harmless). + +.IP "\fBDsnNotification" +Errors encountered during attempt to send delivery status notification. + +.IP "\fBDsnSuppressed" +Delivery status notification (DSN) intentionally suppressed. + +.IP "\fBExtraModules" +Additional code modules Amavis loaded during runtime. + +.IP "\fBFakeSender" +Forged sender addresses, as determimed by Amavis. + +.IP "\fBFatal" +Fatal events. These are presented at the top of the report, as they may require attention. + +.IP "\fBLocalDeliverySkipped" +Failures delivering to a local address. + +.IP "\fBMalwareByScanner" +Breakdown of malware by scanner(s) that detected the malware. + +.IP "\fBMimeError" +Errors encountered during MIME extraction. + +.IP "\fBPanic" +Panic events. These are presented at the top of the report, as they may require attention. + +.IP "\fBp0f" +Passive fingerprint (p0f) hits, grouped by mail contents type (virus, unchecked, banned, spam, ham), +next by operating system genre, and finally by IP address. +Note: Windows systems are refined by Windows OS version, whereas versions of other operating systems +are grouped generically. + +.IP "\fBReleased" +Messages that were released from Amavis quarantine. + +.IP "\fBSADiags" +Diagnostics as reported from SpamAssassin. + +.IP "\fBSmtpResponse" +SMTP responses received during dialog with MTA. These log entries are primarly debug. + +.IP "\fBTmpPreserved" +Temporary directories preserved by Amavis when some component encounters a problem or failure. +Directories listed and their corresponding log entries should be evaluated for problems. + +.IP "\fBVirusScanSkipped" +Messages that could not be scanned by a virus scanner. + +.IP "\fBWarning" +Warning events not categorized in specific warnings below. +These are presented at the top of the report, as they may require attention. + +.IP "\fBWarningAddressModified" +Incomplete email addresses modified by Amavis for safety. + +.IP "\fBWarningNoQuarantineId" +Attempts to release a quarantined message that did not contain an X-Quarantine-ID header. + +.IP "\fBWarningSecurity \fIlevelspec\fR" +Insecure configuration or utility used by Amavis. + +.IP "\fBWarningSmtpShutdown" +Failures during SMTP conversation with MTA. + +.IP "\fBWarningSql" +Failures to communicate with, or error replies from, SQL service. + +.IP "\fBWhitelisted" +Messages that were (soft-)whitelisted. See also Blacklisted above. + +.PD +.SH LEVEL CONTROL +.ad +.fi +The \fBDetailed\fR section of the report consists of a number of sub-sections, +each of which is controlled both globally and independently. +Two settings influence the output provided in the \fBDetailed\fR report: +a global detail level (specified with \fB--detail\fR) which has final (big hammer) +output-limiting control over the \fBDetailed\fR section, +and sub-section specific detail settings (small hammer), which allow further limiting +of the output for a sub-section. +Each sub-section may be limited to a specific depth level, and each sub-level may be limited with top N or threshold limits. +The \fIlevelspec\fR argument to each of the level limiters listed above is used to accomplish this. + +It is probably best to continue explanation of sub-level limiting with the following well-known outline-style hierarchy, and +some basic examples: +.nf + + level 0 + level 1 + level 2 + level 3 + level 4 + level 4 + level 2 + level 3 + level 4 + level 4 + level 4 + level 3 + level 4 + level 3 + level 1 + level 2 + level 3 + level 4 +.fi +.PP +The simplest form of output limiting suppresses all output below a specified level. +For example, a \fIlevelspec\fR set to "2" shows only data in levels 0 through 2. +Think of this as collapsing each sub-level 2 item, thus hiding all inferior levels (3, 4, ...), +to yield: +.nf + + level 0 + level 1 + level 2 + level 2 + level 1 + level 2 +.fi +.PP +Sometimes the volume of output in a section is too great, and it is useful to suppress any data that does not exceed a certain threshold value. +Consider a dictionary spam attack, which produces very lengthy lists of hit-once recipient email or IP addresses. +Each sub-level in the hierarchy can be threshold-limited by setting the \fIlevelspec\fR appropriately. +Setting \fIlevelspec\fR to the value "2::5" will suppress any data at level 2 that does not exceed a hit count of 5. +.PP +Perhaps producing a top N list, such as top 10 senders, is desired. +A \fIlevelspec\fR of "3:10:" limits level 3 data to only the top 10 hits. +.PP +With those simple examples out of the way, a \fIlevelspec\fR is defined as a whitespace- or comma-separated list of one or more of the following: +.IP "\fIl\fR" +Specifies the maximum level to be output for this sub-section, with a range from 0 to 10. +if \fIl\fR is 0, no levels will be output, effectively disabling the sub-section +(level 0 data is already provided in the Summary report, so level 1 is considered the first useful level in the \fBDetailed\fR report). +Higher values will produce output up to and including the specified level. +.IP "\fIl\fB.\fIn\fR" +Same as above, with the addition that \fIn\fR limits this section's level 1 output to +the top \fIn\fR items. +The value for \fIn\fR can be any integer greater than 1. +(This form of limiting has less utility than the syntax shown below. It is provided for +backwards compatibility; users are encouraged to use the syntax below). +.IP "\fIl\fB:\fIn\fB:\fIt\fR" +This triplet specifies level \fIl\fR, top \fIn\fR, and minimum threshold \fIt\fR. +Each of the values are integers, with \fIl\fR being the level limiter as described above, \fIn\fR being +a top \fIn\fR limiter for the level \fIl\fR, and \fIt\fR being the threshold limiter for level \fIl\fR. +When both \fIn\fR and \fIt\fR are specified, \fIn\fR has priority, allowing top \fIn\fR lists (regardless of +threshold value). +If the value of \fIl\fR is omitted, the specified values for \fIn\fR and/or \fIt\fR are used for +all levels available in the sub-section. +This permits a simple form of wildcarding (eg. place minimum threshold limits on all levels). +However, specific limiters always override wildcard limiters. +The first form of level limiter may be included in \fIlevelspec\fR to restrict output, regardless of how many triplets are present. +.PP +All three forms of limiters are effective only when \fBamavis-logwatch\fR's detail level is 5 +or greater (the \fBDetailed\fR section is not activated until detail is at least 5). +.PP +See the \fBEXAMPLES\fR section for usage scenarios. +.SH CONFIGURATION FILE +.ad +\fBAmavis-logwatch\fR can read configuration settings from a configuration file. +Essentially, any command line option can be placed into a configuration file, and +these settings are read upon startup. + +Because \fBamavis-logwatch\fR can run either standalone or within Logwatch, +to minimize confusion, \fBamavis-logwatch\fR inherits Logwatch's configuration +file syntax requirements and conventions. +These are: +.IP \(bu 4'. +White space lines are ignored. +.IP \(bu 4'. +Lines beginning with \fB#\fR are ignored +.IP \(bu 4'. +Settings are of the form: +.nf + + \fIoption\fB = \fIvalue\fR + +.fi +.IP \(bu 4'. +Spaces or tabs on either side of the \fB=\fR character are ignored. +.IP \(bu 4'. +Any \fIvalue\fR protected in double quotes will be case-preserved. +.IP \(bu 4'. +All other content is reduced to lowercase (non-preserving, case insensitive). +.IP \(bu 4'. +All \fBamavis-logwatch\fR configuration settings must be prefixed with "\fB$amavis_\fR" or +\fBamavis-logwatch\fR will ignore them. +.IP \(bu 4'. +When running under Logwatch, any values not prefixed with "\fB$amavis_\fR" are +consumed by Logwatch; it only passes to \fBamavis-logwatch\fR (via environment variable) +settings it considers valid. +.IP \(bu 4'. +The values \fBTrue\fR and \fBYes\fR are converted to 1, and \fBFalse\fR and \fBNo\fR are converted to 0. +.IP \(bu 4'. +Order of settings is not preserved within a configuration file (since settings are passed +by Logwatch via environment variables, which have no defined order). +.PP +To include a command line option in a configuration file, +prefix the command line option name with the word "\fB$amavis_\fR". +The following configuration file setting and command line option are equivalent: +.nf + + \fB$amavis_Line_Style = Truncate\fR + + \fB--line_style Truncate\fR + +.fi +Level limiters are also prefixed with \fB$amavis_\fR, but on the command line are specified with the \fB--limit\fR option: +.nf + + \fB$amavis_SpamBlocked = 2\fR + + \fB--limit SpamBlocked=2\fR + +.fi + + +The order of command line options and configuration file processing occurs as follows: +1) The default configuration file is read if it exists and no \fB--config_file\fR was specified on a command line. +2) Configuration files are read and processed in the order found on the command line. +3) Command line options override any options already set either via command line or from any configuration file. + +Command line options are interpreted when they are seen on the command line, and later options will override previously set options. + + +.SH "EXIT STATUS" +.na +.nf +.ad +.fi +The \fBamavis-logwatch\fR utility exits with a status code of 0, unless an error +occurred, in which case a non-zero exit status is returned. +.SH "EXAMPLES" +.na +.nf +.ad +.fi +.SS Running Standalone +\fBNote:\fR \fBamavis-logwatch\fR reads its log data from one or more named Amavis log files, or from STDIN. +For brevity, where required, the examples below use the word \fIfile\fR as the command line +argument meaning \fI/path/to/amavis.log\fR. +Obviously you will need to substitute \fIfile\fR with the appropriate path. +.nf +.PP +To run \fBamavis-logwatch\fR in standalone mode, simply run: +.nf +.RS 4 +.PP +\fBamavis-logwatch \fIfile\fR +.RE 0 +.nf +.PP +A complete list of options and basic usage is available via: +.nf +.RS 4 +.PP +\fBamavis-logwatch --help\fR +.RE 0 +.nf +.PP +To print a summary only report of Amavis log data: +.nf +.RS 4 +.PP +\fBamavis-logwatch --detail 1 \fIfile\fR +.RE 0 +.fi +.PP +To produce a summary report and a one-level detail report for May 25th: +.nf +.RS 4 +.PP +\fBgrep 'May 25' \fIfile\fB | amavis-logwatch --detail 5\fR +.RE 0 +.fi +.PP +To produce only a top 10 list of Sent email domains, the summary report and detailed reports +are first disabled. Since commands line options are read and enabled left-to-right, +the Sent section is re-enabled to level 1 with a level 1 top 10 limiter: +.nf +.RS 4 +.PP +\fBamavis-logwatch --nosummary --nodetail \\ + --limit spamblocked '1 1:10:' \fIfile\fR +.RE 0 +.fi +.PP +The following command and its sample output shows a more complex level limiter example. +The command gives the top 4 spam blocked recipients (level 1), and under with each recipient +the top 2 sending IPs (level 2) and finally below that, only envelope from addresses (level 3) with hit counts +greater than 6. +Ellipses indicate top N or threshold-limited data: +.nf +.RS 4 +.PP +\fBamavis-logwatch --nosummary --nodetail \\ + --limit spamblocked '1:4: 2:2: 3::6' \fIfile\fR +.nf + +19346 Spam blocked ----------------------------------- + 756 joe@example.com + 12 10.0.0.1 + 12 <> + 12 10.99.99.99 + 12 <> + ... + 640 fred@example.com + 8 10.0.0.1 + 8 <> + 8 192.168.3.19 + 8 <> + ... + 595 peter@sample.net + 8 10.0.0.1 + 8 <> + 7 192.168.3.3 + 7 <> + ... + 547 paul@example.us + 8 192.168.3.19 + 8 <> + 7 10.0.0.1 + 7 <> + ... + ... +.fi +.RE 0 +.fi +.SS Running within Logwatch +\fBNote:\fR Logwatch versions prior to 7.3.6, unless configured otherwise, required the \fB--print\fR option to print to STDOUT instead of sending reports via email. +Since version 7.3.6, STDOUT is the default output destination, and the \fB--print\fR option has been replaced +by \fB--output stdout\fR. Check your configuration to determine where report output will be directed, and add the appropriate option to the commands below. +.PP +To print a summary report for today's Amavis log data: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range today --detail 1\fR +.RE 0 +.nf +.PP +To print a report for today's Amavis log data, with one level +of detail in the \fBDetailed\fR section: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range today --detail 5\fR +.RE 0 +.fi +.PP +To print a report for yesterday, with two levels of detail in the \fBDetailed\fR section: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range yesterday --detail 6\fR +.RE 0 +.fi +.PP +To print a report from Dec 12th through Dec 14th, with four levels of detail in the \fBDetailed\fR section: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range \\ + 'between 12/12 and 12/14' --detail 8\fR +.RE 0 +.PP +To print a report for today, with all levels of detail: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range today --detail 10\fR +.RE 0 +.PP +Same as above, but leaves long lines uncropped: +.nf +.RS 4 +.PP +\fBlogwatch --service amavis --range today --detail 11\fR +.RE 0 +.SS "Amavis Log Level" +.PP +Amavis provides additional log information when the variable +\fB$log_level\fR is increased above the default 0 value. +This information is used by the \fBamavis-logwatch\fR utility to provide additional reports, +not available with the default \fB$log_level\fR=0 value. +A \fB$log_level\fR of 2 is suggested. +.PP +If you prefer not to increase the noise level in your main mail or Amavis logs, +you can configure syslog to log Amavis' output to multiple log files, +where basic log entries are routed to your main mail log(s) and more detailed +entries routed to an Amavis-specific log file used to feed the \fBamavis-logwatch\fR utility. +.PP +A convenient way to accomplish this is to change the Amavis +configuration variables in \fBamavisd.conf\fR as shown below: +.nf + + amavisd.conf: + $log_level = 2; + $syslog_facility = 'local5'; + $syslog_priority = 'debug'; + +.fi +.PP +This increases \fB$log_level\fR to 2, and sends Amavis' log entries to +an alternate syslog facility (eg. \fBlocal5\fR, user), which can then be +routed to one or more log files, including your main mail log file: +.nf + + syslog.conf: + #mail.info -/var/log/maillog + mail.info;local5.notice -/var/log/maillog + + local5.info -/var/log/amavisd-info.log + +.fi +.PP +\fBAmavis\fR' typical \fB$log_level\fR 0 messages will be directed to both your maillog +and to the \fBamavisd-info.log\fR file, but higher \fB$log_level\fR messages +will only be routed to the \fBamavisd-info.log\fR file. +For additional information on Amavis' logging, search the +file \fBRELEASE_NOTES\fR in the Amavis distribution for: +.nf + + "syslog priorities are now dynamically derived" + +.fi +.SH "ENVIRONMENT" +.na +.nf +.ad +.fi +The \fBamavis-logwatch\fR program uses the following (automatically set) environment +variables when running under Logwatch: +.IP \fBLOGWATCH_DETAIL_LEVEL\fR +This is the detail level specified with the Logwatch command line argument \fB--detail\fR +or the \fBDetail\fR setting in the ...conf/services/amavis.conf configuration file. +.IP \fBLOGWATCH_DEBUG\fR +This is the debug level specified with the Logwatch command line argument \fB--debug\fR. +.IP \fBamavis_\fIxxx\fR +The Logwatch program passes all settings \fBamavis_\fIxxx\fR in the configuration file ...conf/services/amavis.conf +to the \fBamavis\fR filter (which is actually named .../scripts/services/amavis) via environment variable. +.SH "FILES" +.na +.nf +.SS Standalone mode +.IP "/usr/local/bin/amavis-logwatch" +The \fBamavis-logwatch\fR program +.IP "/usr/local/etc/amavis-logwatch.conf" +The \fBamavis-logwatch\fR configuration file in standalone mode +.SS Logwatch mode +.IP "/etc/logwatch/scripts/services/amavis" +The Logwatch \fBamavis\fR filter +.IP "/etc/logwatch/conf/services/amavis.conf" +The Logwatch \fBamavis\fR filter configuration file +.SH "SEE ALSO" +.na +.nf +logwatch(8), system log analyzer and reporter +.SH "README FILES" +.na +.ad +.nf +README, an overview of \fBamavis-logwatch\fR +Changes, the version change list history +Bugs, a list of the current bugs or other inadequacies +Makefile, the rudimentary installer +LICENSE, the usage and redistribution licensing terms +.SH "LICENSE" +.na +.nf +.ad +Covered under the included MIT/X-Consortium License: +http://www.opensource.org/licenses/mit-license.php + +.SH "AUTHOR(S)" +.na +.nf +Mike Cappella + +.fi +The original \fBamavis\fR Logwatch filter was written by +Jim O'Halloran, and has had many contributors over the years. +They are entirely not responsible for any errors, problems or failures since the current author's +hands have touched the source code. diff --git a/amavis-logwatch.1.html b/amavis-logwatch.1.html new file mode 100644 index 0000000..24a0af2 --- /dev/null +++ b/amavis-logwatch.1.html @@ -0,0 +1,888 @@ + + + + Man page: amavis-logwatch(1) +
+AMAVIS-LOGWATCH(1)          General Commands Manual         AMAVIS-LOGWATCH(1)
+
+
+
+NAME
+       amavis-logwatch - An Amavisd-new log parser and analysis utility
+
+SYNOPSIS
+       amavis-logwatch [options] [logfile ...]
+
+DESCRIPTION
+       The  amavis-logwatch(1)  utility is an Amavisd-new log parser that pro-
+       duces summaries, details, and statistics  regarding  the  operation  of
+       Amavisd-new (henceforth, simply called Amavis).
+
+       This utility can be used as a standalone program, or as a Logwatch fil-
+       ter module to produce Amavisd-new summary  and  detailed  reports  from
+       within Logwatch.
+
+       Amavis-logwatch  is  able  to produce a wide range of reports with data
+       grouped and sorted as much as possible to reduce  noise  and  highlight
+       patterns.   Brief  summary  reports provide a quick overview of general
+       Amavis operations and message delivery, calling out warnings  that  may
+       require  attention.   Detailed reports provide easy to scan, hierarchi-
+       cally-arranged and organized information, with as much or little detail
+       as desired.
+
+       Much  of  the  interesting data is available when Amavis' $log_level is
+       set to at least 2.  See Amavis Log Level below.
+
+       Amavis-logwatch outputs two principal sections: a Summary section and a
+       Detailed section.  For readability and quick scanning, all event or hit
+       counts appear in the left column, followed by brief description of  the
+       event  type, and finally additional statistics or count representations
+       may appear in the rightmost column.
+
+       The following segment from a sample Summary report illustrates:
+
+           ****** Summary ********************************************
+
+                  9   Miscellaneous warnings
+
+              20313   Total messages scanned ----------------  100.00%
+           1008.534M  Total bytes scanned                1,057,524,252
+           ========   ================================================
+
+               1190   Blocked -------------------------------    5.86%
+                 18     Malware blocked                          0.09%
+                  4     Banned name blocked                      0.02%
+                416     Spam blocked                             2.05%
+                752     Spam discarded (no quarantine)           3.70%
+
+              19123   Passed --------------------------------   94.14%
+                 47     Bad header passed                        0.23%
+              19076     Clean passed                            93.91%
+           ========   ================================================
+
+                 18   Malware -------------------------------    0.09%
+                 18     Malware blocked                          0.09%
+
+                  4   Banned --------------------------------    0.02%
+                  4     Banned file blocked                      0.02%
+
+               1168   Spam ----------------------------------    5.75%
+                416     Spam blocked                             2.05%
+                752     Spam discarded (no quarantine)           3.70%
+
+              19123   Ham -----------------------------------   94.14%
+                 47     Bad header passed                        0.23%
+              19076     Clean passed                            93.91%
+           ========   ================================================
+
+               1982   SpamAssassin bypassed
+                 32   Released from quarantine
+                  2   DSN notification (debug supplemental)
+                  2   Bounce unverifiable
+               2369   Whitelisted
+                  2   Blacklisted
+                 12   MIME error
+                 58   Bad header (debug supplemental)
+                 40   Extra code modules loaded at runtime
+
+       The report indicates there were 9 general warnings, and Amavis  scanned
+       a  total  of  20313  messages  for  a  total  of  1008.53  megabytes or
+       1,057,524,252 bytes.  The next  summary  groups  shows  the  Blocked  /
+       Passed overview, with 1190 Blocked messages (broken down as 18 messages
+       blocked as malware, 4 messages with banned names,  416  spam  messages,
+       and  752  discarded  messages),  and 19123 Passed messages (47 messages
+       with bad headers and 19076 clean messages).
+
+       The next (optional) summary grouping shows message disposition by  con-
+       tents  category.  There were 18 malware messages and 4 banned file mes-
+       sages (all blocked), 1168 Spam messages,  of  which  416  were  blocked
+       (quarantined)  and  752  discarded.  Finally, there were 19123 messages
+       consdidered to be Ham (i.e. not spam), 47 of which contained bad  head-
+       ers.
+
+       Additional count summaries for a variety of events are also listed.
+
+       There are dozens of sub-sections available in the Detailed report, each
+       of whose output can be controlled in various  ways.   Each  sub-section
+       attempts to group and present the most meaningful data at superior lev-
+       els, while pushing less useful or noisy data towards  inferior  levels.
+       The  goal is to provide as much benefit as possible from smart grouping
+       of data, to allow faster report scanning, pattern  identification,  and
+       problem  solving.   Data is always sorted in descending order by count,
+       and then numerically by IP address or alphabetically as appropriate.
+
+       The following Spam blocked segment from a sample Detailed report illus-
+       trates the basic hierarchical level structure of amavis-logwatch:
+
+           ****** Detailed *******************************************
+
+              19346   Spam blocked -----------------------------------
+                756      from@example.com
+                 12         10.0.0.2
+                 12            <>
+                 12         192.168.2.2
+                 12            <>
+                  5         192.168.2.1
+                ...
+
+
+       The  amavis-logwatch  utility reads from STDIN or from the named Amavis
+       logfile.  Multiple logfile arguments may be specified,  each  processed
+       in  order.   The user running amavis-logwatch must have read permission
+       on each named log file.
+
+   Options
+       The options listed  below  affect  the  operation  of  amavis-logwatch.
+       Options specified later on the command line override earlier ones.  Any
+       option may be abbreviated to an unambiguous length.
+
+
+       --[no]autolearn
+       --show_autolearn boolean
+              Enables (disables) output of the autolearn report.  This  report
+              is only available if the default Amavis $log_templ has been mod-
+              ified to provide autolearn results in log entries.  This can  be
+              done  by  uncommenting  two  lines  in the Amavis program itself
+              (where the default log templates reside), or by correctly adding
+              the  $log_templ  variable to the amavisd.conf file.  See Amavis'
+              README.customize and search near the end of the Amavisd  program
+              for "autolearn".
+
+       --[no]by_ccat_summary
+       --show_by_ccat_summary boolean
+              Enables  (disables) the by contents category summary in the Sum-
+              mary section.  Default: enabled.
+
+       -f config_file
+       --config_file config_file
+              Use an alternate configuration file config_file instead  of  the
+              default.  This option may be used more than once.  Multiple con-
+              figuration files will be processed in the order presented on the
+              command line.  See CONFIGURATION FILE below.
+
+       --debug keywords
+              Output  debug  information  during  the operation of amavis-log-
+              watch.  The parameter keywords is one or  more  comma  or  space
+              separated  keywords.   To obtain the list of valid keywords, use
+              --debug xxx where xxx is any invalid keyword.
+
+       --detail level
+              Sets the maximum detail  level  for  amavis-logwatch  to  level.
+              This  option  is  global,  overriding  any other output limiters
+              described below.
+
+              The  amavis-logwatch  utility  produces  a  Summary  section,  a
+              Detailed  section,  and  additional report sections.  With level
+              less than 5, amavis-logwatch will produce only the Summary  sec-
+              tion.  At level 5 and above, the Detailed section, and any addi-
+              tional report sections are candidates for output.   Each  incre-
+              mental  increase  in level generates one additional hierarchical
+              sub-level of output in the Detailed section of the  report.   At
+              level  10, all levels are output.  Lines that exceed the maximum
+              report width (specified  with  max_report_width)  will  be  cut.
+              Setting  level to 11 will prevent lines in the report from being
+              cut (see also --line_style).
+
+       --[no]first_recip_only
+       --show_first_recip_only boolean
+              Specifies whether or not to sort by, and show,  only  the  first
+              recipient when a scanned messages contains multiple recipients.
+
+       --help Print  usage  information  and a brief description about command
+              line options.
+
+       --ipaddr_width width
+              Specifies that IP addresses in address/hostname pairs should  be
+              printed  with a field width of width characters.  Increasing the
+              default may be useful for systems using long IPv6 addresses.
+
+       -l limiter=levelspec
+       --limit limiter=levelspec
+              Sets the level limiter limiter with the specification levelspec.
+
+       --line_style style
+              Specifies how to handle long report  lines.   Three  styles  are
+              available: full, truncate, and wrap.  Setting style to full will
+              prevent cutting lines to max_report_width; this is  what  occurs
+              when  detail  is  11  or  higher.   When  style is truncate (the
+              default),  long   lines   will   be   truncated   according   to
+              max_report_width.   Setting style to wrap will wrap lines longer
+              than max_report_width such that left column hit counts  are  not
+              obscured.   This  option  takes  precedence  over the line style
+              implied by the detail level.  The  options  --full,  --truncate,
+              and --wrap are synonyms.
+
+
+       --nodetail
+              Disables  the Detailed section of the report, and all supplemen-
+              tal reports.  This option provides  a  convenient  mechanism  to
+              quickly  disable  all  sections under the Detailed report, where
+              subsequent command line options may re-enable one or  more  sec-
+              tions to create specific reports.
+
+       --sarules `S,H'
+       --sarules default
+              Enables  the SpamAssassin Rules Hit report.  The comma-separated
+              S and H arguments are top N values for the Spam and Ham reports,
+              respectively, and can be any integer greater than or equal to 0,
+              or the keyword all.   The  keyword  default  uses  the  built-in
+              default values.
+
+       --nosarules
+              Disables the SpamAssassin Rules Hit report.
+
+       --sa_timings nrows
+              Enables the SpamAssassin Timings percentiles report.  The report
+              can be limited to the top N rows with the nrows argument.   This
+              report requires Amavis 2.6+ and SpamAssassin 3.3+.
+
+       --sa_timings_percentiles `P1 [P2 ...]'
+              Specifies  the  percentiles  shown  in  the SpamAssassin Timings
+              report.  The arguments P1 ... are integers from 0 to 100  inclu-
+              sive.  Their order will be preserved in the report.
+
+       --nosa_timings
+              Disables the SpamAssassin Timings report.
+
+       --version
+              Print amavis-logwatch version information.
+
+       --score_frequencies `B1 [B2 ...]'
+       --score_frequencies default
+              Enables  the  Spam Score Frequency report.  The arguments B1 ...
+              are frequency distribution buckets, and can be any real numbers.
+              Their  order  will  be  preserved  in  the  report.  The keyword
+              default uses the built-in default values.
+
+       --noscore_frequencies
+              Disables the Spam Score Frequency report.
+
+       --score_percentiles `P1 [P2 ...]'
+       --score_percentiles default
+              Enables the Spam Score Percentiles report.  The arguments P1 ...
+              specify  the  percentiles  shown in the report, and are integers
+              from 0 to 100 inclusive.  The keyword default uses the  built-in
+              default values.
+
+       --noscore_percentiles
+              Disables the Spam Score Percentiles report.
+
+
+       --[no]sect_vars
+       --show_sect_vars boolean
+              Enables  (disables)  supplementing  each  Detailed section title
+              with the name of that section's level limiter.   The  name  dis-
+              played  is  the command line option (or configuration file vari-
+              able) used to limit that section's output.  With the large  num-
+              ber  of level limiters available in amavis-logwatch, this a con-
+              venient mechanism for determining exactly  which  level  limiter
+              affects a section.
+
+       --[no]startinfo
+       --show_startinfo boolean
+              Enables (disables) the Amavis startup report showing most recent
+              Amavis startup details.
+
+       --[no]summary
+
+       --show_summary
+              Enables (disables) displaying of the the Summary section of  the
+              report.   The variable Amavis_Show_Summary in used in a configu-
+              ration file.
+
+       --syslog_name namepat
+              Specifies the syslog service name that amavis-logwatch  uses  to
+              match  syslog  lines.  Only log lines whose service name matches
+              the perl regular expression namepat will be used by  amavis-log-
+              watch;  all  non-matching  lines  are silently ignored.  This is
+              useful when a pre-installed Amavis package  uses  a  name  other
+              than the default (amavis).
+
+              Note: if you use parenthesis in your regular expression, be sure
+              they are cloistering and not capturing: use  (?:pattern) instead
+              of (pattern).
+
+       --timings percent
+              Enables  the Amavis Scan Timings percentiles report.  The report
+              can be top N-percent limited with the percent argument.
+
+       --timings_percentiles `P1 [P2 ...]'
+              Specifies the percentiles shown in the Scan Timings report.  The
+              arguments  P1  ...  are integers from 0 to 100 inclusive.  Their
+              order will be preserved in the report.
+
+       --notimings
+              Disables the Amavis Scan Timings report.
+
+       --version
+              Print amavis-logwatch version information.
+
+
+   Level Limiters
+       The output of every section in the Detailed report is controlled  by  a
+       level  limiter.   The name of the level limiter variable will be output
+       when the sect_vars option is set.  Level limiters are  set  either  via
+       command  line in standalone mode with --limit limiter=levelspec option,
+       or via configuration  file  variable  $amavis_limiter=levelspec.   Each
+       limiter  requires  a  levelspec  argument,  which is described below in
+       LEVEL CONTROL.
+
+       The list of level limiters is shown below.
+
+
+       Amavis major contents category (ccatmajor) sections, listed in order of
+       priority: VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH, OVERSIZED, MTA,
+       CLEAN.
+
+       MalwareBlocked
+       MalwarePassed
+              Blocked or passed  messages  that  contain  malware  (ccatmajor:
+              VIRUS).
+
+       BannedNameBlocked
+       BannedNamePassed
+              Blocked  or  passed  messages  that contain banned names in MIME
+              parts (ccatmajor: BANNED).
+
+       UncheckedBlocked
+       UncheckedPassed
+              Blocked or passed messages that were  not  checked  by  a  virus
+              scanner or SpamAssassin (Amavis ccatmajor: UNCHECKED).
+
+       SpamBlocked
+       SpamPassed
+              Blocked  or  passed  messages  that  were  considered  spam that
+              reached kill level (Amavis ccatmajor: SPAM)
+
+       SpammyBlocked
+       SpammyPassed
+              Blocked or passed messages that were considered  spam,  but  did
+              not reach kill level (Amavis ccatmajor: SPAMMY)
+
+       BadHeaderBlocked
+       BadHeaderPassed
+              Blocked  or passed messages that contain bad mail headers (ccat-
+              major: BAD-HEADER).
+
+       OversizedBlocked
+       OversizedPassed
+              Blocked  or  passed  messages  that  were  considered  oversized
+              (Amavis ccatmajor: OVERSIZED).
+
+       MtaBlocked
+       MtaPassed
+              Blocked  or  passed  messages due to failure to re-inject to MTA
+              (Amavis ccatmajor:  MTA-BLOCKED).   Occurrences  of  this  event
+              indicates a configuration problem.  [ note: I don't believe mta-
+              passed occurs, but exists for completeness.]
+
+       OtherBlocked
+       OtherPassed
+              Blocked or passed messages that are not any of other major  con-
+              tents categories (Amavis ccatmajor: OTHER).
+
+
+       TempFailBlocked
+       TempfailPassed
+              Blocked  or passed messages that had a temporary failure (Amavis
+              ccatmajor: TEMPFAIL)
+
+       CleanBlocked
+       CleanPassed
+              Messages blocked or passed which were considered  clean  (Amavis
+              ccatmajor: CLEAN; i.e. non-spam, non-viral).
+
+       Other sections, arranged alphabetically:
+
+       AvConnectFailure
+              Problems connecting to Anti-Virus scanner(s).
+
+       AvTimeout
+              Timeouts awaiting responses from Anti-Virus scanner(s).
+
+       ArchiveExtract
+              Archive extraction problems.
+
+       BadHeaderSupp
+              Supplemental debug information regarding messages containing bad
+              mail headers.
+
+       Bayes  Messages frequencies by Bayesian probability buckets.
+
+       BadAddress
+              Invalid mail address syntax.
+
+       Blacklisted
+              Messages that were  (soft-)blacklisted.   See  also  Whitelisted
+              below.
+
+       BounceKilled
+       BounceRescued
+       BounceUnverifiable
+              Disposition of incoming bounce messages (DSNs).
+
+       ContentType
+              MIME attachment breakdown by type/subtype.
+
+       DccError
+              Errors encountered with or returned by DCC.
+
+       DefangError
+              Errors encountered during defang process.
+
+       Defanged
+              Messages defanged (rendered harmless).
+
+       DsnNotification
+              Errors  encountered during attempt to send delivery status noti-
+              fication.
+
+       DsnSuppressed
+              Delivery status notification (DSN) intentionally suppressed.
+
+       ExtraModules
+              Additional code modules Amavis loaded during runtime.
+
+       FakeSender
+              Forged sender addresses, as determimed by Amavis.
+
+       Fatal  Fatal events.  These are presented at the top of the report,  as
+              they may require attention.
+
+       LocalDeliverySkipped
+              Failures delivering to a local address.
+
+       MalwareByScanner
+              Breakdown of malware by scanner(s) that detected the malware.
+
+       MimeError
+              Errors encountered during MIME extraction.
+
+       Panic  Panic  events.  These are presented at the top of the report, as
+              they may require attention.
+
+       p0f    Passive fingerprint (p0f) hits, grouped by  mail  contents  type
+              (virus,  unchecked, banned, spam, ham), next by operating system
+              genre, and finally by IP address.   Note:  Windows  systems  are
+              refined by Windows OS version, whereas versions of other operat-
+              ing systems are grouped generically.
+
+       Released
+              Messages that were released from Amavis quarantine.
+
+       SADiags
+              Diagnostics as reported from SpamAssassin.
+
+       SmtpResponse
+              SMTP responses received  during  dialog  with  MTA.   These  log
+              entries are primarly debug.
+
+       TmpPreserved
+              Temporary  directories  preserved  by Amavis when some component
+              encounters a problem or failure.  Directories listed  and  their
+              corresponding log entries should be evaluated for problems.
+
+       VirusScanSkipped
+              Messages that could not be scanned by a virus scanner.
+
+       Warning
+              Warning  events  not  categorized  in  specific  warnings below.
+              These are presented at the  top  of  the  report,  as  they  may
+              require attention.
+
+       WarningAddressModified
+              Incomplete email addresses modified by Amavis for safety.
+
+       WarningNoQuarantineId
+              Attempts  to  release a quarantined message that did not contain
+              an X-Quarantine-ID header.
+
+       WarningSecurity levelspec
+              Insecure configuration or utility used by Amavis.
+
+       WarningSmtpShutdown
+              Failures during SMTP conversation with MTA.
+
+       WarningSql
+              Failures to communicate with, or error replies  from,  SQL  ser-
+              vice.
+
+       Whitelisted
+              Messages  that  were  (soft-)whitelisted.   See also Blacklisted
+              above.
+
+
+LEVEL CONTROL
+       The Detailed section of the report consists of  a  number  of  sub-sec-
+       tions,  each  of  which  is controlled both globally and independently.
+       Two settings influence the output provided in the  Detailed  report:  a
+       global detail level (specified with --detail) which has final (big ham-
+       mer) output-limiting control over the Detailed section, and sub-section
+       specific  detail  settings (small hammer), which allow further limiting
+       of the output for a sub-section.  Each sub-section may be limited to  a
+       specific  depth  level, and each sub-level may be limited with top N or
+       threshold limits.  The levelspec argument to each of the level limiters
+       listed above is used to accomplish this.
+
+       It  is probably best to continue explanation of sub-level limiting with
+       the following well-known outline-style hierarchy, and some basic  exam-
+       ples:
+
+           level 0
+              level 1
+                 level 2
+                    level 3
+                       level 4
+                       level 4
+                 level 2
+                    level 3
+                       level 4
+                       level 4
+                       level 4
+                    level 3
+                       level 4
+                    level 3
+              level 1
+                 level 2
+                    level 3
+                       level 4
+
+       The  simplest  form  of  output  limiting suppresses all output below a
+       specified level.  For example, a levelspec set to "2" shows  only  data
+       in  levels  0  through 2.  Think of this as collapsing each sub-level 2
+       item, thus hiding all inferior levels (3, 4, ...), to yield:
+
+           level 0
+              level 1
+                 level 2
+                 level 2
+              level 1
+                 level 2
+
+       Sometimes the volume of output in a section is too  great,  and  it  is
+       useful  to  suppress  any data that does not exceed a certain threshold
+       value.  Consider a dictionary spam attack, which produces very  lengthy
+       lists  of  hit-once recipient email or IP addresses.  Each sub-level in
+       the hierarchy can be threshold-limited by setting the levelspec  appro-
+       priately.  Setting levelspec to the value "2::5" will suppress any data
+       at level 2 that does not exceed a hit count of 5.
+
+       Perhaps producing a top N list, such as top 10 senders, is desired.   A
+       levelspec of "3:10:" limits level 3 data to only the top 10 hits.
+
+       With  those simple examples out of the way, a levelspec is defined as a
+       whitespace- or comma-separated list of one or more of the following:
+
+       l      Specifies the maximum level to be output for  this  sub-section,
+              with a range from 0 to 10.  if l is 0, no levels will be output,
+              effectively disabling the sub-section (level 0 data  is  already
+              provided  in  the  Summary  report, so level 1 is considered the
+              first useful level in the Detailed report).  Higher values  will
+              produce output up to and including the specified level.
+
+       l.n    Same  as  above,  with the addition that n limits this section's
+              level 1 output to the top n items.  The value for n can  be  any
+              integer greater than 1.  (This form of limiting has less utility
+              than the syntax shown below. It is provided for  backwards  com-
+              patibility; users are encouraged to use the syntax below).
+
+       l:n:t  This  triplet specifies level l, top n, and minimum threshold t.
+              Each of the values are integers, with l being the level  limiter
+              as described above, n being a top n limiter for the level l, and
+              t being the threshold limiter for level l.  When both  n  and  t
+              are  specified, n has priority, allowing top n lists (regardless
+              of threshold value).  If the value of l is omitted,  the  speci-
+              fied  values for n and/or t are used for all levels available in
+              the sub-section.  This permits a simple form of wildcarding (eg.
+              place  minimum  threshold  limits on all levels).  However, spe-
+              cific limiters always override  wildcard  limiters.   The  first
+              form  of  level limiter may be included in levelspec to restrict
+              output, regardless of how many triplets are present.
+
+       All three forms of limiters are effective only  when  amavis-logwatch's
+       detail  level  is  5  or greater (the Detailed section is not activated
+       until detail is at least 5).
+
+       See the EXAMPLES section for usage scenarios.
+
+CONFIGURATION FILE
+       Amavis-logwatch can read configuration settings  from  a  configuration
+       file.   Essentially,  any command line option can be placed into a con-
+       figuration file, and these settings are read upon startup.
+
+       Because amavis-logwatch can run either standalone or  within  Logwatch,
+       to  minimize  confusion, amavis-logwatch inherits Logwatch's configura-
+       tion file syntax requirements and conventions.  These are:
+
+       o   White space lines are ignored.
+
+       o   Lines beginning with # are ignored
+
+       o   Settings are of the form:
+
+                   option = value
+
+
+       o   Spaces or tabs on either side of the = character are ignored.
+
+       o   Any value protected in double quotes will be case-preserved.
+
+       o   All other content is reduced  to  lowercase  (non-preserving,  case
+           insensitive).
+
+       o   All  amavis-logwatch  configuration  settings must be prefixed with
+           "$amavis_" or amavis-logwatch will ignore them.
+
+       o   When  running  under  Logwatch,  any  values  not   prefixed   with
+           "$amavis_"  are consumed by Logwatch; it only passes to amavis-log-
+           watch (via environment variable) settings it considers valid.
+
+       o   The values True and Yes are converted to 1, and False  and  No  are
+           converted to 0.
+
+       o   Order  of  settings  is  not  preserved within a configuration file
+           (since settings are passed by Logwatch via  environment  variables,
+           which have no defined order).
+
+       To  include  a  command line option in a configuration file, prefix the
+       command line option name with the word "$amavis_".  The following  con-
+       figuration file setting and command line option are equivalent:
+
+               $amavis_Line_Style = Truncate
+
+               --line_style Truncate
+
+       Level limiters are also prefixed with $amavis_, but on the command line
+       are specified with the --limit option:
+
+               $amavis_SpamBlocked = 2
+
+               --limit SpamBlocked=2
+
+
+
+       The order of command line options  and  configuration  file  processing
+       occurs  as  follows:  1)  The  default configuration file is read if it
+       exists and no --config_file was specified on a command line.   2)  Con-
+       figuration  files are read and processed in the order found on the com-
+       mand line.  3) Command line options override any  options  already  set
+       either via command line or from any configuration file.
+
+       Command  line options are interpreted when they are seen on the command
+       line, and later options will override previously set options.
+
+
+
+EXIT STATUS
+       The amavis-logwatch utility exits with a status code of  0,  unless  an
+       error occurred, in which case a non-zero exit status is returned.
+
+EXAMPLES
+   Running Standalone
+       Note:  amavis-logwatch reads its log data from one or more named Amavis
+       log files, or from STDIN.  For brevity, where  required,  the  examples
+       below   use  the  word  file  as  the  command  line  argument  meaning
+       /path/to/amavis.log.  Obviously you will need to substitute  file  with
+       the appropriate path.
+
+       To run amavis-logwatch in standalone mode, simply run:
+
+           amavis-logwatch file
+
+       A complete list of options and basic usage is available via:
+
+           amavis-logwatch --help
+
+       To print a summary only report of Amavis log data:
+
+           amavis-logwatch --detail 1 file
+
+       To produce a summary report and a one-level detail report for May 25th:
+
+           grep 'May 25' file | amavis-logwatch --detail 5
+
+       To produce only a top 10 list of Sent email domains, the summary report
+       and detailed reports are first disabled. Since  commands  line  options
+       are  read  and enabled left-to-right, the Sent section is re-enabled to
+       level 1 with a level 1 top 10 limiter:
+
+           amavis-logwatch --nosummary --nodetail \
+              --limit spamblocked '1 1:10:' file
+
+       The following command and its sample output shows a more complex  level
+       limiter  example.   The command gives the top 4 spam blocked recipients
+       (level 1), and under with each recipient the top 2 sending  IPs  (level
+       2)  and finally below that, only envelope from addresses (level 3) with
+       hit counts greater than 6.  Ellipses indicate top N  or  threshold-lim-
+       ited data:
+
+           amavis-logwatch --nosummary --nodetail \
+                   --limit spamblocked '1:4: 2:2: 3::6' file
+
+           19346   Spam blocked -----------------------------------
+             756      joe@example.com
+              12         10.0.0.1
+              12            <>
+              12         10.99.99.99
+              12            <>
+                     ...
+             640      fred@example.com
+               8         10.0.0.1
+               8            <>
+               8         192.168.3.19
+               8            <>
+                     ...
+             595      peter@sample.net
+               8         10.0.0.1
+               8            <>
+               7         192.168.3.3
+               7            <>
+                     ...
+             547      paul@example.us
+               8         192.168.3.19
+               8            <>
+               7         10.0.0.1
+               7            <>
+                      ...
+                   ...
+
+   Running within Logwatch
+       Note:  Logwatch  versions  prior to 7.3.6, unless configured otherwise,
+       required the --print option to  print  to  STDOUT  instead  of  sending
+       reports  via  email.  Since version 7.3.6, STDOUT is the default output
+       destination, and the --print option has been replaced by --output  std-
+       out.  Check your configuration to determine where report output will be
+       directed, and add the appropriate option to the commands below.
+
+       To print a summary report for today's Amavis log data:
+
+           logwatch --service amavis --range today --detail 1
+
+       To print a report for today's Amavis log data, with one level
+       of detail in the Detailed section:
+
+           logwatch --service amavis --range today --detail 5
+
+       To print a report for yesterday, with  two  levels  of  detail  in  the
+       Detailed section:
+
+           logwatch --service amavis --range yesterday --detail 6
+
+       To  print  a report from Dec 12th through Dec 14th, with four levels of
+       detail in the Detailed section:
+
+           logwatch --service amavis --range \
+                   'between 12/12 and 12/14' --detail 8
+
+       To print a report for today, with all levels of detail:
+
+           logwatch --service amavis --range today --detail 10
+
+       Same as above, but leaves long lines uncropped:
+
+           logwatch --service amavis --range today --detail 11
+
+   Amavis Log Level
+       Amavis provides additional log information when the variable $log_level
+       is  increased  above  the default 0 value.  This information is used by
+       the amavis-logwatch utility to provide additional reports,  not  avail-
+       able  with  the  default $log_level=0 value.  A $log_level of 2 is sug-
+       gested.
+
+       If you prefer not to increase the noise level  in  your  main  mail  or
+       Amavis logs, you can configure syslog to log Amavis' output to multiple
+       log files, where basic log entries are routed to your main mail  log(s)
+       and more detailed entries routed to an Amavis-specific log file used to
+       feed the amavis-logwatch utility.
+
+       A convenient way to accomplish this is to change the Amavis  configura-
+       tion variables in amavisd.conf as shown below:
+
+           amavisd.conf:
+               $log_level = 2;
+               $syslog_facility = 'local5';
+               $syslog_priority = 'debug';
+
+
+       This  increases  $log_level  to  2, and sends Amavis' log entries to an
+       alternate syslog facility (eg. local5, user), which can then be  routed
+       to one or more log files, including your main mail log file:
+
+           syslog.conf:
+               #mail.info                         -/var/log/maillog
+               mail.info;local5.notice            -/var/log/maillog
+
+               local5.info                        -/var/log/amavisd-info.log
+
+
+       Amavis'  typical  $log_level  0  messages will be directed to both your
+       maillog and to the amavisd-info.log file, but  higher  $log_level  mes-
+       sages will only be routed to the amavisd-info.log file.  For additional
+       information on Amavis' logging, search the file  RELEASE_NOTES  in  the
+       Amavis distribution for:
+
+           "syslog priorities are now dynamically derived"
+
+
+ENVIRONMENT
+       The  amavis-logwatch  program  uses  the  following (automatically set)
+       environment variables when running under Logwatch:
+
+       LOGWATCH_DETAIL_LEVEL
+              This is the detail level specified  with  the  Logwatch  command
+              line argument --detail or the Detail setting in the ...conf/ser-
+              vices/amavis.conf configuration file.
+
+       LOGWATCH_DEBUG
+              This is the debug level specified with the Logwatch command line
+              argument --debug.
+
+       amavis_xxx
+              The  Logwatch program passes all settings amavis_xxx in the con-
+              figuration file ...conf/services/amavis.conf to the amavis  fil-
+              ter  (which  is  actually named .../scripts/services/amavis) via
+              environment variable.
+
+FILES
+   Standalone mode
+       /usr/local/bin/amavis-logwatch
+              The amavis-logwatch program
+
+       /usr/local/etc/amavis-logwatch.conf
+              The amavis-logwatch configuration file in standalone mode
+
+   Logwatch mode
+       /etc/logwatch/scripts/services/amavis
+              The Logwatch amavis filter
+
+       /etc/logwatch/conf/services/amavis.conf
+              The Logwatch amavis filter configuration file
+
+SEE ALSO
+       logwatch(8), system log analyzer and reporter
+
+README FILES
+       README, an overview of amavis-logwatch
+       Changes, the version change list history
+       Bugs, a list of the current bugs or other inadequacies
+       Makefile, the rudimentary installer
+       LICENSE, the usage and redistribution licensing terms
+
+LICENSE
+       Covered under the included MIT/X-Consortium License:
+       http://www.opensource.org/licenses/mit-license.php
+
+
+AUTHOR(S)
+       Mike Cappella
+
+       The original amavis Logwatch filter was written by Jim O'Halloran, and
+       has had many contributors over the years.  They are entirely not
+       responsible for any errors, problems or failures since the current
+       author's hands have touched the source code.
+
+
+
+                                                            AMAVIS-LOGWATCH(1)
+
diff --git a/amavis-logwatch.conf b/amavis-logwatch.conf new file mode 100644 index 0000000..2166461 --- /dev/null +++ b/amavis-logwatch.conf @@ -0,0 +1,224 @@ + +########################################################################### +# $Id: amavis.conf,v 1.10 2007/05/16 04:57:16 mrc Exp $ +########################################################################### + +# You can put comments anywhere you want to. They are effective for the +# rest of the line. + +# this is in the format of = . Whitespace at the beginning +# and end of the lines is removed. Whitespace before and after the = sign +# is removed. Everything is case *insensitive*. + +# Yes = True = On = 1 +# No = False = Off = 0 + +Title = "Amavisd-new" + +# Which logfile group... +LogFile = maillog + +# Specifies the global maximum detail level +# +#Detail = 10 + +# Only give lines pertaining to the amavis service... +# +# The variables OnlyService and amavis_Syslog_Name are regular +# expression patterns, and should both be set to match the +# amavis service name used in your syslog entries. +#*OnlyService = (usr/sbin/amavisd|dccproc) +*OnlyService = (amavis|dccproc) +*RemoveHeaders + +# Set this to the service name used in amavis syslog entries +# +# Note: if you use parenthesis in your regular expression, be sure it +# is cloistering and not capturing: use (?:pattern) instead of (pattern). +# $amavis_Syslog_Name = "/usr/sbin/amavisd" +$amavis_Syslog_Name = "(?:amavis|dccproc)" + +# +# Set the variable below to specify the maximum report width. +# for Detail <= 10 +# +$amavis_Max_Report_Width = 100 + +# Specifies how to handle line lengths greater than Max_Report_Width. +# Options are Truncate (default), Wrap, or Full. +# for Detail <= 10 +# +$amavis_Line_Style = Truncate + +# Show names of detail section variables/command line options in +# detail report titles. For command line, use --[no]sect_vars, +# without an argument. +# +$amavis_Show_Sect_Vars = No + +# In reports, for tallying purposes, use (and show) only the first +# recipient when a message contains multple recipients. +# +$amavis_Show_First_Recip_Only = No + +# Include a by-contents category grouping in the summary report. +# +$amavis_Show_By_Ccat_Summary = Yes + +# Amavis Timings Report +# +# Specifies the percentiles of collected data to show in the timing report. +# Valid values are from 0 to 100, inclusive. +# +$amavis_Timings_Percentiles = "0 5 25 50 75 95 100" + +# +# Show top N percent of the amavis scan timings report +# +$amavis_Timings = 95 + +# Amavis SpamAssassin Timings Report +# +# Specifies the percentiles of collected data to show in the +# SpamAssassin timing report (requires amavis 2.6+, SA 3.3+). +# Valid values are from 0 to 100, inclusive. +# +$amavis_SA_Timings_Percentiles = "0 5 25 50 75 95 100" + +# +# Show top N rows of the SpamAssassin timings report +# Requires: amavis 2.6+, SpamAssassin 3.3+ +# +$amavis_SA_Timings = 100 + +# Spam Score Percentiles Report +# +# Specifies the percentiles shown in the spam scores frequency report. +# Valid values range from 0 to 100, inclusive. The keyword "default" +# can be used instead to reset the values to their built-in default. +# +$amavis_Score_Percentiles = "0 50 90 95 98 100" + +# Spam Score Frequency Report +# +# Specifies the buckets shown in the spam scores frequency report. +# Valid values are real numbers. The keyword "default" +# can be used instead to reset the values to their built-in default. +# +$amavis_Score_Frequencies = "-10 -5 0 5 10 20 30" + +# SpamAssassin Spam / Ham Rules Hit Report +# +# Specifies the number of top S spam and top H ham hits to show in the +# SpamAssassin Spam and Ham rules hit report. The value is a list +# separated by whitespace or a comma. The order is "spam,ham". The +# keyword "all" means unlimited limit, and 0 specifies none. For +# example, the value "all,10" would show all Spam rules hit, but only +# the top 10 ham rules, whereas "0,all" would prevent the Spam hit +# report, and show all the hit Ham rules. +# +$amavis_SARules = "20 20" + +# Autolearn Report +# +# Shows the autolearn report when autolearn entries are present in +# amavis log entries. To make these available, the default +# $log_templ variable needs to be modified. This can be done by +# uncommenting two lines in the amavis program itself (where the +# default log templates reside), by correctly adding the $log_templ +# variable to the amavisd.conf file. See amavis' README.customize +# and the end of the amavisd program, searching for "autolearn". +$amavis_Show_Autolearn = Yes + +# If available, show most recent amavis startup details +# +$amavis_Show_StartInfo = Yes + +# Show the summary section. For command line, use --[no]summary, +# without an argument. +$amavis_Show_Summary = Yes + +# Level Limiters +# +# The variables below control the maximum output level for a given +# category. A level of 1 indicates only one level of detailed output +# in the Detailed report section. The Summary section is only avail- +# able at logwatch --Detail level >= 5. Increasing the Detail level +# by one adds one level of additional detail in the Summary section. +# +# For example, Detail 5 would output one additional level of detail, +# Detail 6 two levels, etc. all the way up to 10. Finally, Detail +# 11 yields uncropped lines of output. +# +# You can control the maximum number of level 1 lines by appending +# a period and a number. The value 2.10 would indicate 2 levels +# of detail, but only 10 level 1 lines. For example, setting +# $amavis_SpamBlocked = 1.20 yields a top 20 list of blocked spam. +# +# A more useful form of limiting uses triplets in the form l:n:t. +# This triplet specifies level l, top n, and minimum threshold t. +# Each of the values are integers, with l being the level limiter +# as described above, n being a top n limiter for the level l, and +# t being the threshold limiter for level l. When both n and t +# are specified, n has priority, allowing top n lists (regardless +# of threshold value). If the value of l is omitted, the speci- +# fied values for n and/or t are used for all levels available in +# the sub-section. This permits a simple form of wildcarding (eg. +# place minimum threshold limits on all levels). However, spe- +# cific limiters always override wildcard limiters. The first +# form of level limiter may be included in levelspec to restrict +# output, regardless of how many triplets are present. + +$amavis_CleanPassed = 0 +$amavis_CleanBlocked = 10 +$amavis_SpamPassed = 10 +$amavis_SpamBlocked = 10 +$amavis_SpammyPassed = 10 +$amavis_SpammyBlocked = 10 +$amavis_MalwarePassed = 10 +$amavis_MalwareBlocked = 10 +$amavis_BannedNamePassed = 10 +$amavis_BannedNameBlocked = 10 +$amavis_BadHeaderPassed = 10 +$amavis_BadHeaderBlocked = 10 +$amavis_MTABlocked = 10 +$amavis_OversizedBlocked = 10 +$amavis_OtherBlocked = 10 + +$amavis_AVConnectFailure = 10 +$amavis_AVTimeout = 10 +$amavis_ArchiveExtract = 10 +$amavis_BadHeaderSupp = 10 +$amavis_Bayes = 10 +$amavis_Blacklisted = 10 +$amavis_BounceKilled = 10 +$amavis_BounceRescued = 10 +$amavis_BounceUnverifiable = 10 +$amavis_ContentType = 10 +$amavis_DccError = 10 +$amavis_DefangError = 10 +$amavis_Defanged = 10 +$amavis_DsnNotification = 10 +$amavis_DsnSuppressed = 10 +$amavis_ExtraModules = 10 +$amavis_FakeSender = 10 +$amavis_LocalDeliverySkipped = 10 +$amavis_MalwareByScanner = 10 +$amavis_MalwareToSpam = 10 +$amavis_MimeError = 10 +$amavis_p0f = 2 +$amavis_Released = 10 +$amavis_SADiags = 10 +$amavis_SmtpResponse = 10 +$amavis_TmpPreserved = 10 +$amavis_VirusScanSkipped = 1 +$amavis_Warning = 10 +$amavis_WarningAddressModified = 2 +$amavis_WarningNoQuarantineID = 1 +$amavis_WarningSecurity = 10 +$amavis_WarningSmtpShutdown = 10 +$amavis_WarningSQL = 10 +$amavis_Whitelisted = 10 + + +# vi: shiftwidth=3 tabstop=3 et -- 2.43.2