A while back, I wrote a quick plugin to show off the basic plugin layout and to make the contributed plugins page not empty. The plugin checked uploaded documents for viruses with ClamAV.

Here's the plugin code, which hooks into the "content/scan" trigger:

<?php

require_once(KT_LIB_DIR . '/plugins/plugin.inc.php');
require_once(KT_LIB_DIR . '/plugins/pluginregistry.inc.php');
require_once(KT_LIB_DIR . '/storage/storagemanager.inc.php');

class ClamAVPlugin extends KTPlugin {
    var $sNamespace = "nbm.clamav.plugin";
    function setup() {
        $this->registerTrigger('content', 'scan', 'ClamAVTrigger', 'nbm.clamav.trigger.scan');
    }

}
$oPluginRegistry =& KTPluginRegistry::getSingleton();
$oPluginRegistry->registerPlugin('ClamAVPlugin', 'nbm.clamav.plugin', __FILE__);

class ClamAVTrigger {
    var $sName;
    var $sDescription;
    var $sDisplayName;

    function ClamAV () {
    }

    function setDocument($oDocument) {
        $this->oDocument = $oDocument;
    }

    function scan() {
        if (PEAR::isError($this->oDocument)) {
            return;
        }
        if (empty($this->oDocument)) {
            return;
        }
        $sCmd = KTUtil::findCommand('clamav/clamdscan', 'clamdscan');
        if ($sCmd === false) {
            return;
        }

        $oStorage = KTStorageManagerUtil::getSingleton();
        $sFile = $oStorage->temporaryFile($this->oDocument);

        $sLastLine = exec(KTUtil::safeShellString(array($sCmd, $sFile)), $aOutput, $iReturnCode);

        $oStorage->freeTemporaryFile($sFile);

        if ($iReturnCode == 1) {
            return new ClamAVError($aOutput);
        }
    }
}

class ClamAVError extends PEAR_Error {
    function ClamAVError($aOutput) {
        $this->aOutput = $aOutput;
        $message = 'A virus was found in your file';
        parent::PEAR_Error($message);
    }
}