ClamAV-Clamd

 view release on metacpan or  search on metacpan

include/clamav/clamd_verdict.h  view on Meta::CPAN

/* clamd_verdict.h - turning a reply into an answer that cannot be
 * misread by accident.
 *
 * Phase 4 of plan_clamav_clamd.
 *
 * FOUR states, because clamd answers OK for files it declined to scan.
 * A boolean API here reports "clean" for precisely the inputs an
 * attacker constructs - a 2 KB nested zip, a password-protected archive,
 * anything over MaxFileSize - and passes every test anybody writes,
 * because nobody writes the test where the answer is "I did not look".
 */
#ifndef CLAMD_VERDICT_H
#define CLAMD_VERDICT_H

/* stream.h, not conn.h: the local-ceiling and stream-cut codes live
 * there, and both of them are verdicts rather than transport failures. */
#include "clamav/clamd_stream.h"

#define CC_CLEAN        0
#define CC_INFECTED     1
#define CC_UNSCANNABLE  2
#define CC_ERROR        3

/* A signature name is REMOTE INPUT. A file crafted to match a chosen
 * signature chooses the string that comes back, and that string ends up
 * in logs and, in a careless consumer, in an HTTP response. It is
 * length-bounded here and handed onward with its length. */
#define CC_SIGLEN  256
#define CC_REASONLEN 64

typedef struct {
    int   state;
    char  signature[CC_SIGLEN];    /* "" when there is none */
    char  reason[CC_REASONLEN];    /* why unscannable; "" otherwise */
} cc_verdict;

/* The two families that mean "clamd did not see the content".
 *
 * NOT every Heuristics.* name: Heuristics.Phishing.* and
 * Heuristics.OLE2.ContainsMacros are clamd saying it LOOKED and thinks
 * the thing is bad, which is a detection. These two are clamd saying it
 * could not look, which is the opposite and must never read as clean. */
#define CC_PFX_LIMITS    "Heuristics.Limits.Exceeded."
#define CC_PFX_ENCRYPTED "Heuristics.Encrypted."

static int cc_starts_with(const char *s, size_t len, const char *pfx) {
    size_t n = strlen(pfx);
    return len >= n && memcmp(s, pfx, n) == 0;
}

static int cc_ends_with(const char *s, size_t len, const char *suf) {
    size_t n = strlen(suf);
    return len >= n && memcmp(s + len - n, suf, n) == 0;
}

static void cc_copy_bounded(char *dst, size_t cap, const char *src, size_t len) {
    if (len >= cap) len = cap - 1;
    if (len) memcpy(dst, src, len);
    dst[len] = '\0';
}

/* Parse one reply.
 *
 * Shapes, all measured rather than assumed:
 *
 *   fd[11]: OK                                        clean
 *   stream: OK                                        clean
 *   fd[11]: Eicar-Test-Signature FOUND                infected
 *   fd[11]: Heuristics.Limits.Exceeded.MaxFileSize FOUND   unscannable
 *   fd[11]: Heuristics.Encrypted.Zip FOUND            unscannable
 *   INSTREAM size limit exceeded. ERROR               unscannable
 *   /x: File path check failure: ... ERROR            error
 *   UNKNOWN COMMAND                                   error (no ERROR suffix!)
 *   anything unrecognised                             error, NEVER clean
 */
static void cc_parse_reply(const char *reply, size_t len, cc_verdict *v) {
    memset(v, 0, sizeof *v);
    v->state = CC_ERROR;

    if (!reply || len == 0) return;

    /* Trim a trailing newline, which the 'n' framing leaves behind. */
    while (len && (reply[len - 1] == '\n' || reply[len - 1] == '\r')) len--;

    if (cc_ends_with(reply, len, " FOUND")) {
        const char *sig;
        size_t siglen, cut = len - 6;      /* strip " FOUND" */
        size_t i;

        /* The description before the signature is "fd[N]: ", "stream: "
         * or a path - and a path may itself contain ": ". Take the LAST
         * ": " so a colon in a filename cannot eat the signature. */
        sig = reply; siglen = cut;
        for (i = cut; i >= 2; i--) {
            if (reply[i - 2] == ':' && reply[i - 1] == ' ') {
                sig = reply + i;
                siglen = cut - i;
                break;
            }
        }



( run in 1.748 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )