module(..., package.seeall) -- Load libraries require("modelfunctions") require("fs") require("format") require("validator") require("luasql.postgres") local DatabaseName = "webproxylog" local DatabaseOwner = "weblogowner" local DatabaseUser = "webloguser" local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " local env local con local configfile = "/etc/weblog/weblog.conf" local configcontent = fs.read_file(configfile) or "" local config = format.parse_ini_file(configcontent, "") or {} local goodwordslist = "/etc/weblog/goodwords" local goodwords local badwordslist = "/etc/weblog/badwords" local badwords local ignorewordslist = "/etc/weblog/ignorewords" local ignorewords local files = {badwordslist, goodwordslist, ignorewordslist, configfile} local database_creation_script = { "CREATE TABLE dbhistlog (logdatetime timestamp(3) without time zone NOT NULL, msgtext text)", "CREATE TABLE pubweblog(sourcename character varying(40), clientip inet NOT NULL, clientuserid character varying(64) NOT NULL, logdatetime timestamp(3) without time zone NOT NULL, uri text NOT NULL, bytes bigint NOT NULL, reason text, score integer, shortreason text, badyesno int, deniedyesno int, bypassyesno int, wordloc text, goodwordloc text, selected boolean, id serial)", "CREATE TABLE pubweblog_history(sourcename character varying(40), clientip inet NOT NULL, clientuserid character varying(64) NOT NULL, logdatetime timestamp(3) without time zone NOT NULL, uri text NOT NULL, bytes bigint NOT NULL, reason text, score integer, shortreason text, badyesno int, deniedyesno int, bypassyesno int, wordloc text, goodwordloc text, selected boolean, id int)", "CREATE TABLE weblog(sourcename character varying(40), clientip inet NOT NULL, clientuserid character varying(64) NOT NULL, logdatetime timestamp(3) without time zone NOT NULL, uri text NOT NULL, bytes bigint NOT NULL, reason text, score integer, shortreason text, badyesno int, deniedyesno int, bypassyesno int, wordloc text, goodwordloc text)", "CREATE TABLE source (sourcename character varying(40) NOT NULL, method character varying(100) NOT NULL, userid character varying(32), passwd character varying(255), source character varying(255) NOT NULL, tzislocal boolean, enabled boolean)", "CREATE TABLE usagestat (sourcename character varying(40) NOT NULL, date timestamp(0) without time zone NOT NULL, numrequest integer, numblock integer)", "ALTER TABLE ONLY source ADD CONSTRAINT source_pkey PRIMARY KEY (sourcename)", "CREATE INDEX dbhistlogdatetimeidx ON dbhistlog USING btree (logdatetime)", "CREATE INDEX pubweblogclientdateidx ON pubweblog USING btree (logdatetime, clientuserid)", "CREATE INDEX pubweblogclientuserididx ON pubweblog USING btree (clientuserid)", "CREATE INDEX pubwebloglogdatetimeidx ON pubweblog USING btree (logdatetime)", "CREATE INDEX pubweblog_historyclientdateidx ON pubweblog_history USING btree (logdatetime, clientuserid)", "CREATE INDEX pubweblog_historyclientuserididx ON pubweblog_history USING btree (clientuserid)", "CREATE INDEX pubweblog_historylogdatetimeidx ON pubweblog_history USING btree (logdatetime)", "GRANT SELECT ON dbhistlog TO "..DatabaseUser, "GRANT SELECT ON pubweblog TO "..DatabaseUser, "GRANT SELECT ON pubweblog_history TO "..DatabaseUser, "GRANT SELECT, UPDATE, INSERT, DELETE ON source TO "..DatabaseUser, "GRANT SELECT ON usagestat TO "..DatabaseUser, } -- ################################################################################ -- DATABASE FUNCTIONS local function assert (v, m) if not v then m = m or "Assertion failed!" error(m, 0) end return v, m end -- Escape special characters in sql statements and truncate to length local escape = function(sql, length) sql = sql or "" if length then sql = string.sub(sql, 1, length) end return string.gsub(sql, "'", "''") end -- List the postgres databases on this system local listdatabases = function() local dbs = {} local cmd = path.."psql -U postgres -tl 2>&1" local f = io.popen(cmd) local result = f:read("*a") or "" f:close() for line in string.gmatch(result, "[^\n]+") do dbs[#dbs+1] = string.match(line, "^ (%S+)") end return dbs end -- Create the necessary database local createdatabase = function(password) local result = {} -- First, create the users local cmd = path..'psql -U postgres -c "CREATE USER '..DatabaseOwner..' WITH PASSWORD \''..password..'\'" 2>&1' local f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() cmd = path..'psql -U postgres -c "CREATE USER '..DatabaseUser..'" 2>&1' f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() -- Create the database cmd = path..'psql -U postgres -c "CREATE DATABASE '..DatabaseName..' WITH OWNER '..DatabaseOwner..'" 2>&1' f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() return table.concat(result, "\n") end -- Delete the database and roles local deletedatabase = function() local result = {} local cmd = path..'psql -U postgres -c "DROP DATABASE '..DatabaseName..'" 2>&1' local f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() cmd = path..'psql -U postgres -c "DROP ROLE '..DatabaseUser..'" 2>&1' f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() cmd = path..'psql -U postgres -c "DROP ROLE '..DatabaseOwner..'" 2>&1' f = io.popen(cmd) table.insert(result, f:read("*a")) f:close() return table.concat(result, "\n") end -- Run an SQL script local runSQLscript = function(filename) -- Create the database local cmd = path..'psql -U postgres -f "'..filename..'" '..DatabaseName..' 2>&1' local f = io.popen(cmd) local result = f:read("*a") or "" f:close() -- Create the tables print (result) return result end -- Create the database and tables -- pg_dump -U postgres -c webproxylog > makeweblog.postgres --runSQLscript("/root/work/weblog/makeweblog.postgres") local databaseconnect = function(username, password) if not con then -- create environment object env = assert (luasql.postgres()) -- connect to data source con = assert (env:connect(DatabaseName, username, password)) end end local databasedisconnect = function() if env then env:close() env = nil end if con then con:close() con = nil end end local logme = function(message) local sql = string.format("INSERT INTO dbhistlog VALUES ('now', '%s')", escape(message)) local res = assert (con:execute(sql)) end local listhistorylogentries = function() local entries = {} -- retrieve a cursor cur = assert (con:execute"SELECT logdatetime, msgtext from dbhistlog ORDER BY logdatetime") row = cur:fetch ({}, "a") while row do entries[#entries+1] = {logdatetime = row.logdatetime, msgtext = row.msgtext} row = cur:fetch (row, "a") end -- close everything cur:close() return entries end local importlogentry = function(entry, sourcename) if entry then local sql = string.format("INSERT INTO weblog VALUES ('%s', '%s', '%s', '%s', '%s', '%s','%s','%s','%s','%s','%s','%s','%s')", escape(sourcename), escape(entry.clientip), escape(entry.clientuserid, 64):lower(), escape(entry.logdatetime), escape(entry.URL), escape(entry.bytes), escape(entry.reason), escape(entry.score or "0"), escape(entry.shortreason), escape(entry.badyesno or "0"), escape(entry.deniedyesno or "0"), escape(entry.bypassyesno or "0"), escape(entry.wordloc), escape(entry.goodwordloc)) local res = assert (con:execute(sql)) end end local listsourceentries = function(sourcename) local sources = {} -- retrieve a cursor local sql = "SELECT sourcename, method, userid, passwd, source, tzislocal, enabled FROM source" if sourcename then sql = sql .. " WHERE sourcename='" .. escape(sourcename) .. "'" end sql = sql .. " ORDER BY sourcename" cur = assert (con:execute(sql)) row = cur:fetch ({}, "a") while row do row.tzislocal = (row.tzislocal == "t") row.enabled = (row.enabled == "t") sources[#sources+1] = row row = cur:fetch ({}, "a") end cur:close() return sources end local importsourceentry = function(source) local sql = string.format("INSERT INTO source VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')", escape(source.sourcename), escape(source.method), escape(source.userid), escape(source.passwd), escape(source.source), escape(tostring(source.tzislocal):upper()), escape(tostring(source.enabled):upper())) local res = assert (con:execute(sql)) return res end local updatesourceentry = function(source) local sql = string.format("UPDATE source SET method='%s', userid='%s', passwd='%s', source='%s', tzislocal='%s', enabled='%s' WHERE sourcename='%s'", escape(source.method), escape(source.userid), escape(source.passwd), escape(source.source), escape(tostring(source.tzislocal):upper()), escape(tostring(source.enabled):upper()), escape(source.sourcename)) local res = assert (con:execute(sql)) return res end local deletesourceentry = function(sourcename) local sql = string.format("DELETE FROM source WHERE sourcename='%s'", escape(sourcename)) local res = assert (con:execute(sql)) return res end -- Generate usage statistics from weblog and blocklog local updateusagestats = function() -- update the usagestat table from weblog -- (will result in multiple rows where logs rotated on partial hours) local sql = "insert into usagestat select weblog.sourcename, " .. "date_trunc('hour', weblog.logdatetime) as date, " .. "count(*), SUM(deniedyesno) from weblog group by sourcename,date" local res = assert (con:execute(sql)) end -- Move weblog into pubweblog, and truncate weblog local importpubweblog = function() local sql = "ANALYZE" res = assert (con:execute(sql)) -- Move weblog to pubweblog sql= "insert into pubweblog select * from weblog" res = assert (con:execute(sql)) logme("importpubweblog imported " .. res .. " new rows into database.") -- Truncate the staging table assert (con:execute("truncate weblog")) logme("truncated staging table") end -- Delete useage stats from more than a year ago local groomusagestat = function() local res = assert (con:execute("delete from usagestat where " .. "date < (now() - INTERVAL '1 year')")) logme("removed " .. res .. " old usage status lines") end -- Delete history log information from more than a month ago local groomdbhistlog = function() local res = assert (con:execute("delete from dbhistlog where " .. "logdatetime < (now() - INTERVAL '1 month')")) logme("removed " .. res .. " old dbhistlog lines") end -- Delete old junk from pub tables local groompublogs = function() local purgedays = config.purgedays or 30 local now = os.time() local temp = os.date("%Y-%m-%d %H:%M:%S", now - purgedays*86400) logme("Purgedate is " .. temp .. ". Nothing will exist in pubweblog from before purgedate.") -- Move flagged records to histoy and then purge anything older than purgedate sql = "Insert into pubweblog_history select * from pubweblog where logdatetime < '" .. temp .."' and (badyesno > 0 or deniedyesno > 0 or bypassyesno > 0 or selected = 'true')" res = assert (con:execute(sql)) logme("Moved " .. res .. " old records to history") sql = "Delete from pubweblog where logdatetime < '" .. temp .."'" res = assert (con:execute(sql)) logme("Deleted " .. res .. " old records from pubweblog") sql = "delete from pubweblog_history where logdatetime < (now() - INTERVAL '1 year')" res = assert (con:execute(sql)) logme("Deleted " .. res .. " old records from pubweblog_history") end local generatewhereclause = function(clientuserid, starttime, endtime, clientip, badyesno, deniedyesno, bypassyesno, score, urisearch, selected) local sql = "" local where = {} if clientuserid and clientuserid ~= "" then where[#where+1] = "clientuserid LIKE '%"..escape(clientuserid).."%'" end if starttime and starttime ~= "" then where[#where+1] = "logdatetime >= '"..escape(starttime).."'" end if endtime and endtime ~= "" then where[#where+1] = "logdatetime <= '"..escape(endtime).."'" end if clientip and clientip ~= "" then where[#where+1] = "clientip = '"..escape(clientip).."'" end if badyesno then where[#where+1] = "badyesno = '1'" end if deniedyesno then where[#where+1] = "deniedyesno = '1'" end if bypassyesno then where[#where+1] = "bypassyesno = '1'" end if score and score ~= "" then where[#where+1] = "score >= '"..escape(score).."'" end if urisearch and urisearch ~= "" then where[#where+1] = "lower(uri) LIKE '%"..escape(urisearch).."%'" end if selected then where[#where+1] = "selected = 'true'" end if #where > 0 then sql = " WHERE " .. table.concat(where, " AND ") end return sql end local listlogentries = function(activelog, clientuserid, starttime, endtime, clientip, badyesno, deniedyesno, bypassyesno, score, urisearch, sortby, selected) local entries = {} -- retrieve a cursor local sql = "SELECT * FROM "..activelog sql = sql .. generatewhereclause(clientuserid, starttime, endtime, clientip, badyesno, deniedyesno, bypassyesno, score, urisearch, selected) sql = sql .. " ORDER BY "..sortby cur = assert (con:execute(sql)) row = cur:fetch ({}, "a") while row do if config.shorturi == "true" then shorturi=string.gsub(row.uri, "[;?].*", "...") end entries[#entries+1] = {sourcename=row.sourcename, clientip=row.clientip, clientuserid=row.clientuserid, logdatetime=row.logdatetime, uri=row.uri, shorturi=shorturi, bytes=row.bytes, reason=row.reason, score=row.score, shortreason=row.shortreason, badyesno=row.badyesno, deniedyesno=row.deniedyesno, bypassyesno=row.bypassyesno, wordloc=row.wordloc, id=row.id, selected=row.selected } if (config.shortreason ~= "true") then entries[#entries].shortreason = nil end row = cur:fetch (row, "a") end -- close everything cur:close() return entries end local groupflaggedlogentries = function(starttime, endtime, groupby) groupby = groupby or "clientuserid" local entries = {} -- retrieve a cursor --local sql = "SELECT "..groupby..", count(*) AS numblock, max(score) AS maxscore FROM pubweblog" local sql = "SELECT "..groupby..", COUNT(*) as numrecords, SUM(CASE WHEN (bypassyesno > '0' OR deniedyesno > '0' OR badyesno > '0') THEN 1 ELSE 0 END) as numflagged, sum(score) AS numhits, sum(CASE WHEN deniedyesno > '0' THEN 1 ELSE 0 END) AS numdenied, sum(CASE WHEN bypassyesno > '0' THEN 1 ELSE 0 END) AS numbypassed, max(score) as maxscore from pubweblog" --sql = sql .. generatewhereclause(nil, starttime, endtime) .. " AND deniedyesno > '0'" sql = sql .. generatewhereclause(nil, starttime, endtime) --sql = sql .. " GROUP BY "..groupby.. " ORDER BY numblock DESC" sql = sql .. " GROUP BY " ..groupby.. " ORDER BY numflagged DESC" cur = assert (con:execute(sql)) row = cur:fetch ({}, "a") while row do --entries[#entries+1] = {numblock=row.numblock, maxscore=row.maxscore} entries[#entries+1] = {numrecords=row.numrecords, numflagged=row.numflagged, numhits=row.numhits, numdenied=row.numdenied, numbypassed=row.numbypassed, maxscore=row.maxscore} entries[#entries][groupby] = row[groupby] row = cur:fetch (row, "a") end -- close everything cur:close() return entries end local listusagestats = function() local entries = {} -- retrieve a cursor local sql = "SELECT sourcename, date, sum(numrequest) AS numrequest, sum(numblock) AS numblock " .. "FROM usagestat GROUP BY sourcename, date ORDER BY date, sourcename" cur = assert (con:execute(sql)) row = cur:fetch ({}, "a") while row do entries[#entries+1] = {sourcename=row.sourcename, date=row.date, numrequest=row.numrequest, numblock=row.numblock} row = cur:fetch (row, "a") end -- close everything cur:close() return entries end local testdatabaseentry = function(datatype, value) local success = true local errtxt local sql = "CREATE TEMP TABLE testing ( test "..datatype.." DEFAULT '"..escape(value).."' ) ON COMMIT DROP" local res, err = pcall(function() assert (con:execute(sql)) end) if not res then success = false errtxt = string.gsub(err or "", "\n.*", "") end return success, errtxt end local convertdatabaseentry = function(datatype, value) local success = true local errtxt local result = value local res, err = pcall(function() local sql = "CREATE TEMP TABLE testing ( test "..datatype.." )" assert (con:execute(sql)) sql = "INSERT INTO testing VALUES ('"..escape(value).."')" assert (con:execute(sql)) sql = "SELECT * FROM testing" local cur = assert (con:execute(sql)) local row = cur:fetch ({}, "a") if row then result = row.test end end) if not res then success = false errtxt = string.gsub(err or "", "\n.*", "") end local res, err = pcall(function() local sql = "DROP TABLE testing" assert (con:execute(sql)) end) return success, errtxt, result end local printtableentries = function(tablename) -- retrieve a cursor local count = 0 cur = assert (con:execute("SELECT * from "..tablename)) -- print all rows, the rows will be indexed by field names row = cur:fetch ({}, "a") while row do count = count + 1 for name,val in pairs(row) do APP.logevent(name.." = "..val..", ") end row = cur:fetch (row, "a") end -- close everything cur:close() APP.logevent("Table "..tablename.." contains "..count.." rows") end -- ################################################################################ -- LOG FILE FUNCTIONS local function checkwords(logentry) local goodwordloc={} local badwordloc={} local wrdcnt=0 local isbad=0 --check for ignored records first for i,thisline in ipairs(ignorewords) do if not thisline then break end _,instcnt = string.lower(logentry.URL):gsub(thisline, " ") if instcnt ~= 0 then logentry.ignoreme = true --logme("ignoring...") break end end if not logentry.ignoreme then --proceed with record analysis for i,thisline in ipairs(badwords) do if not thisline then break end _,instcnt = string.lower(logentry.URL):gsub(thisline, " ") if instcnt ~= 0 then -- logme("instcnt = "..instcnt) isbad=1 wrdcnt= wrdcnt + instcnt badwordloc[#badwordloc+1] = thisline end end --check for DansGuardian actions if (logentry.reason and logentry.reason ~= "") then if string.find(logentry.reason,"DENIED") then -- logme("*Denied*") logentry.deniedyesno=1 elseif string.find(logentry.URL,"GBYPASS") then -- logme("GBYPASS") logentry.bypassyesno=1 elseif string.find(logentry.reason,"OVERRIDE") then -- logme("*OVERRIDE*") logentry.bypassyesno=1 end end --check for Squark actions if (logentry.squarkaction and logentry.squarkaction ~= "") then --logme("squarkaction="..logentry.squarkaction) if string.find(logentry.squarkaction, "blocked") then logentry.deniedyesno=1 elseif string.find(logentry.squarkaction,"overridden") then logentry.bypassyesno=1 end end for i,goodline in ipairs(goodwords) do if not goodline then break end _,instcnt = string.lower(logentry.URL):gsub(goodline, " ") --if string.find(logentry.URL,goodline) then if instcnt ~= 0 then if wrdcnt >= instcnt then wrdcnt = wrdcnt - instcnt else wrdcnt = 0 end goodwordloc[#goodwordloc+1] = goodline end end end -- Reset bad to reduce number of bad hits if score is zero -- if wrdcnt == 0 then -- isbad=0 -- end logentry.score=wrdcnt logentry.badyesno=isbad logentry.wordloc=table.concat(badwordloc,"|") logentry.gwordloc=table.concat(goodwordloc,"|") end local function parsesquidlog(line) -- Format of squid log (space separated): -- time elapsed remotehost code/status bytes method URL rfc931 peerstatus/peerhost local words = {} for word in string.gmatch(line, "%S+") do words[#words+1] = word end local logentry = {logdatetime=words[1], elapsed=words[2], clientip=words[3], code=string.match(words[4] or "", "^[^/]*"), status=string.match(words[4] or "", "[^/]*$"), bytes=words[5], method=words[6], URL=words[7], clientuserid=words[8], peerstatus=string.match(words[9] or "", "^[^/]*"), peerhost=string.match(words[9] or "", "[^/]*$"), squarkcategory=string.match(words[11] or "", "^[^,]*"), squarkaction=string.match(words[11] or "", "[^,]*$")} checkwords(logentry) -- Don't care about local requests (from DG) (this check also removes blank lines) if logentry.clientip and logentry.clientip ~= "127.0.0.1" then logentry.logdatetime = os.date("%Y-%m-%d %H:%M:%S", logentry.logdatetime)..string.match(logentry.logdatetime, "%..*") return logentry end return nil end local function parsedglog(line) local words = format.string_to_table(line, "\t") local logentry = {logdatetime=words[1], clientuserid=words[2], clientip=words[3], URL=words[4], reason=words[5], method=words[6], bytes=words[7], shortreason=words[9], deniedyesno=1} checkwords(logentry) if logentry.reason and logentry.reason ~= "" then if logentry.shortreason == "" then logentry.shortreason = logentry.reason end --logentry.score = string.match(logentry.reason, "^.*: ([0-9]+) ") logentry.logdatetime = string.gsub(logentry.logdatetime, "%.", "-") return logentry end return nil end -- ################################################################################ -- DOWNLOAD FILE FUNCTIONS -- must do apk_add wget first local connecttosource = function(source, cookiesfile) local success = false logme("Connecting to source "..source.sourcename) if source.method == "http" or source.method == "https" then fs.write_file(cookiesfile, "password="..source.passwd.."&userid="..source.userid.."&Logon=Logon") local cmd = "wget -O - --no-check-certificate --save-cookies "..cookiesfile.." --keep-session-cookies --post-file '"..cookiesfile.."' '"..source.method.."://"..format.escapespecialcharacters(source.source).."/cgi-bin/acf/acf-util/logon/logon' 2>/dev/null" local f = io.popen(cmd) local resultpage = f:read("*a") f:close() if resultpage == "" then logme("Failed to connect to "..source.sourcename) elseif string.find(resultpage, "Log in") then logme("Failed to log in to "..source.sourcename) else success = true end elseif source.method == "local" then success = true end return success end local getlogcandidates = function(source, cookiesfile) local candidates = {} if source.method == "http" or source.method == "https" then local cmd = "wget -O - --no-check-certificate --load-cookies "..cookiesfile.." '"..source.method.."://"..source.source.."/cgi-bin/acf/alpine-baselayout/logfiles/status' 2>/dev/null" local f = io.popen(cmd) local resultpage = f:read("*a") f:close() for file in string.gmatch(resultpage, "download%?name=([^\"]+)") do candidates[#candidates+1] = file end elseif source.method == "local" then candidates = fs.find_files_as_array(nil, source.source) end return candidates end local openlogfile = function(source, cookiesfile, logfile) local handle if source.method == "http" or source.method == "https" then local cmd = "wget -O - --no-check-certificate --load-cookies "..cookiesfile.." --post-data 'name="..logfile.."' '"..source.method.."://"..source.source.."/cgi-bin/acf/alpine-baselayout/logfiles/download' 2>/dev/null" if string.find(logfile, "%.gz$") then cmd = cmd.." | gunzip -c" end handle = io.popen(cmd) elseif source.method == "local" then if string.find(logfile, "%.gz$") then local cmd = "gunzip -c "..logfile handle = io.popen(cmd) else handle = io.open(logfile) end end return handle end local deletelogfile = function(source, cookiesfile, logfile) if source.method == "http" or source.method == "https" then local cmd = "wget -O - --no-check-certificate --load-cookies "..cookiesfile.." --post-data 'name="..logfile.."' '"..source.method.."://"..source.source.."/cgi-bin/acf/alpine-baselayout/logfiles/delete' 2>/dev/null" local f = io.popen(cmd) f:close() elseif source.method == "local" then os.remove(logfile) end end -- ################################################################################ -- PUBLIC FUNCTIONS function getsourcelist() local retval = cfe({ type="list", value={}, label="Weblog Source List" }) local res, err = pcall(function() databaseconnect(DatabaseUser) retval.value = listsourceentries() databasedisconnect() end) if not res then retval.errtxt = err end return retval end function getsource(sourcename) local sourcedata = getnewsource() sourcedata.value.sourcename.value = sourcename sourcedata.value.sourcename.errtxt = "Source name does not exist" local res, err = pcall(function() databaseconnect(DatabaseUser) local sourcelist = listsourceentries() databasedisconnect() for i,source in ipairs(sourcelist) do if source.sourcename == sourcename then sourcedata.value.sourcename.errtxt = nil for name,val in pairs(source) do if sourcedata.value[name] then sourcedata.value[name].value = val end end break end end end) if not res then sourcedata.errtxt = err end return sourcedata end local validatesource = function(sourcedata) local success = modelfunctions.validateselect(sourcedata.value.method) local test = {"sourcename", "source"} if sourcedata.value.method.value ~= "local" then test[#test+1] = "userid" test[#test+1] = "passwd" end for i,name in ipairs(test) do if sourcedata.value[name].value == "" then sourcedata.value[name].errtxt = "Cannot be empty" success = false end end return success end function updatesource(sourcedata) local success = validatesource(sourcedata) sourcedata.errtxt = "Failed to update source" if success then local source = {} for name,val in pairs(sourcedata.value) do source[name] = val.value end local res, err = pcall(function() databaseconnect(DatabaseUser) sourcedata.descr = updatesourceentry(source) databasedisconnect() sourcedata.errtxt = nil end) if not res and err then sourcedata.errtxt = sourcedata.errtxt .. "\n" .. err end end return sourcedata end function getnewsource() local source = {} source.sourcename = cfe({ label="Source Name" }) source.method = cfe({ type="select", value="local", label="Method", option={"http", "https", "local"} }) source.userid = cfe({ label="UserID" }) source.passwd = cfe({ label="Password" }) source.source = cfe({ value="/var/log", label="Source Location / Address" }) source.tzislocal = cfe({ type="boolean", value=false, label="Using local timezone" }) source.enabled = cfe({ type="boolean", value=false, label="Enabled" }) return cfe({ type="group", value=source, label="Source" }) end function createsource(sourcedata) local success = validatesource(sourcedata) sourcedata.errtxt = "Failed to create source" if success then local source = {} for name,val in pairs(sourcedata.value) do source[name] = val.value end -- remove spaces from sourcename source.sourcename = string.gsub(source.sourcename, "%s+$", "") local res, err = pcall(function() databaseconnect(DatabaseUser) sourcedata.descr = importsourceentry(source) databasedisconnect() sourcedata.errtxt = nil end) if not res and err then sourcedata.errtxt = sourcedata.errtxt .. "\n" .. err end end return sourcedata end function deletesource(sourcename) local result = cfe({ errtxt="Failed to delete source", label="Delete source result" }) local res, err = pcall(function() databaseconnect(DatabaseUser) local number = deletesourceentry(sourcename) databasedisconnect() if number > 0 then result.errtxt = nil result.value = "Source Deleted" end end) if not res and err then result.errtxt = result.errtxt .. "\n" .. err end return result end function testsource(sourcename) local result = cfe({ label="Test source result" }) -- temporary override of logme function to capture messages to result.value result.value = {} local temp = logme logme = function(message) table.insert(result.value, message) end local cookiesfile = "/tmp/cookies-"..tostring(os.time()) local res, err = pcall(function() databaseconnect(DatabaseUser) local sources = listsourceentries(sourcename) databasedisconnect() if #sources < 1 then result.errtxt = "Failed to find source" else local source = sources[1] -- run the test if connecttosource(source, cookiesfile) then local files = getlogcandidates(source, cookiesfile) if #files == 0 then logme("No log files found") else for i,file in ipairs(files) do logme("Found log file "..file) end end end end end) if not res then result.errtxt = "Failed to connect to source" if err then result.errtxt = result.errtxt .. "\n" .. err end end os.remove(cookiesfile) -- fix the result result.value = table.concat(result.value, "\n") or "" logme = temp return result end -- import a logfile and delete logfile after local function importlogfile(source, cookiesfile, file, parselog_func) logme("Getting " .. file ) local loghandle = openlogfile(source, cookiesfile, file) logme("Processing " .. file ) local res, err = pcall(function() con:execute("START TRANSACTION") for line in loghandle:lines() do assert(con:execute("SAVEPOINT before_line")) local res2, err2 = pcall(function() local logentry = parselog_func(line) importlogentry(logentry, source.sourcename) end) if not res2 then if (config.stoponerror == "true") then pcall(function() con:execute("ROLLBACK") end) else assert(con:execute("ROLLBACK TO before_line")) con:execute("COMMIT") end pcall(function() logme("Exception on line:"..line) end) if err2 then pcall(function() logme(err2) end) end if (config.stoponerror == "true") then assert(res2, "Import halted on exception") else con:execute("START TRANSACTION") end else assert(con:execute("RELEASE SAVEPOINT before_line")) end end con:execute("COMMIT") end) if not res then pcall(function() con:execute("ROLLBACK") end) if err then pcall(function() logme(err) end) end end loghandle:close() if res then logme("Deleting " .. file ) deletelogfile(source, cookiesfile, file) end return res end function importlogs() local result = cfe({ label="Weblog Import Logs Result" }) local count = 0 local success = true local res, err = pcall(function() databaseconnect(DatabaseOwner, config.password) -- Download, parse, and import the logs logme("Executing importlogs") logme("Analyzing...") local sql = "ANALYZE" res = assert (con:execute(sql)) -- Get the word lists goodwords = fs.read_file_as_array(goodwordslist) or {} badwords = fs.read_file_as_array(badwordslist) or {} ignorewords = fs.read_file_as_array(ignorewordslist) or {} -- Determine sources local sources = listsourceentries(sourcename) for i,source in ipairs(sources) do if source.enabled then logme("Getting logs from source " .. source.sourcename) local cookiesfile = "/tmp/cookies-"..tostring(os.time()) if connecttosource(source, cookiesfile) then local files = getlogcandidates(source, cookiesfile) for j,file in ipairs(files) do if string.match(file, "dansguardian/access%.log[%.%-]") then count = count + 1 success = importlogfile(source, cookiesfile, file, parsedglog) and success elseif string.match(file, "squid/access%.log[%.%-]") then count = count + 1 success = importlogfile(source, cookiesfile, file, parsesquidlog) and success end end end os.remove(cookiesfile) end end -- Process the logs if success then updateusagestats() importpubweblog() end -- Purge old database entries groomusagestat() groomdbhistlog() groompublogs() databasedisconnect() end) if not res or not success then result.errtxt = "Import Logs Failure" if err then pcall(function() logme(err) end) result.errtxt = result.errtxt .. "\n" .. err end pcall(function() databasedisconnect() end) end result.value = "Imported "..tostring(count).." logs" return result end function getactivitylog() local retval = cfe({ type="list", value={}, label="Weblog Activity Log" }) local res, err = pcall(function() databaseconnect(DatabaseUser) retval.value = listhistorylogentries() or {} databasedisconnect() end) if not res then retval.errtxt = err end return retval end function geteditselected() local result = {} result.select = cfe({ type="list", value={}, label="Entries to mark as selected" }) result.deselect = cfe({ type="list", value={}, label="Entries to mark as selected" }) return cfe({ type="group", value=result, label="Select / Deselect log entries" }) end function editselected(data) local res, err = pcall(function() databaseconnect(DatabaseOwner) con:execute("START TRANSACTION") for i,sel in ipairs(data.value.select.value) do assert (con:execute("UPDATE pubweblog SET selected = true WHERE id = '"..escape(sel).."'")) end for i,sel in ipairs(data.value.deselect.value) do assert (con:execute("UPDATE pubweblog SET selected = false WHERE id = '"..escape(sel).."'")) end con:execute("COMMIT") databasedisconnect() end) if not res then data.errtxt = err end return data end function clearselected() local retval = cfe({ label="Clear selected records" }) local res, err = pcall(function() sql = "UPDATE pubweblog SET selected = false WHERE selected = true" databaseconnect(DatabaseOwner) assert (con:execute(sql)) databasedisconnect() retval.value = "Cleared" end) if not res then retval.errtxt = err end return retval end local validateweblogparameters = function(params) local success = modelfunctions.validateselect(params.value.activelog) success = modelfunctions.validateselect(params.value.sortby) and success if params.value.clientip.value ~= "" and string.find(params.value.clientip.value, "[^%d%.]") then params.value.clientip.errtxt = "Invalid IP Address" success = false end if not validator.is_integer(params.value.score.value) then params.value.score.errtxt = "Must be a number" success = false end local res, err = pcall(function() databaseconnect(DatabaseUser) local s if params.value.starttime.value ~= "" then s,params.value.starttime.errtxt,params.value.starttime.value = convertdatabaseentry("TIMESTAMP", params.value.starttime.value) success = success and s end if params.value.endtime.value ~= "" then s,params.value.endtime.errtxt,params.value.endtime.value = convertdatabaseentry("TIMESTAMP", params.value.endtime.value) success = success and s end if params.value.focus.value ~= "" then s,params.value.focus.errtxt,params.value.focus.value = convertdatabaseentry("TIMESTAMP", params.value.focus.value) success = success and s end databasedisconnect() end) if not res and err then params.value.starttime.errtxt = err params.value.endtime.errtxt = err params.value.focus.errtxt = err success = false end return success end function getweblogparameters() local c = getconfig() local result = {} result.activelog = cfe({ type="select", value="pubweblog", option={"pubweblog", "pubweblog_history"}, label="Active Weblog", seq=1 }) result.starttime = cfe({ value=c.value.auditstart.value, label="Start Time", seq=2 }) result.endtime = cfe({ value=c.value.auditend.value, label="End Time", seq=3 }) result.clientuserid = cfe({ value="", label="User ID", seq=4 }) result.clientip = cfe({ value="", label="Client IP", seq=5 }) result.urisearch = cfe({ value="", label="URI Contains", descr="Retrieve records where the URI contains this word", seq=6 }) result.score = cfe({ value=c.value.score.value, label="Minimum Score", descr="Minimum score to search on", seq=7 }) result.sortby = cfe({ type="select", value=c.value.sortby.value, option=c.value.sortby.option, label="Sort By field", descr="Sort by this field when displaying records", seq=8 }) result.badyesno = cfe({ type="boolean", value=c.value.badyesno.value, label="Show Suspect Records", descr="Limit search to records marked as suspect", seq=9 }) result.deniedyesno = cfe({ type="boolean", value=c.value.deniedyesno.value, label="Show Denied Records", descr="Limit search to Denied URIs", seq=10 }) result.bypassyesno = cfe({ type="boolean", value=c.value.bypassyesno.value, label="Show Bypass Records", descr="Limit search to Bypass attempts", seq=11 }) result.selected = cfe({ type="boolean", value=false, label="Show Selected Records", descr="Limit search to records that have been selected", seq=12 }) result.focus = cfe({ value="", label="Focus Time", seq=13 }) return cfe({ type="group", value=result, label="Weblog Access Log" }) end function getweblog(result) local success = validateweblogparameters(result) result.value.log = cfe({ type="list", value={}, label="Weblog Access Log" }) result.value.window = cfe({ value=config.window or "5", label="Time Window" }) local err if success then local res, err = pcall(function() databaseconnect(DatabaseUser) result.value.log.value = listlogentries(result.value.activelog.value, result.value.clientuserid.value, result.value.starttime.value, result.value.endtime.value, result.value.clientip.value, result.value.badyesno.value, result.value.deniedyesno.value, result.value.bypassyesno.value, result.value.score.value, result.value.urisearch.value, result.value.sortby.value, result.value.selected.value ) or {} databasedisconnect() end) if not res then result.errtxt = err end else result.errtxt = "Invalid search parameters" end return result end function getusagestats() local retval = cfe({ type="list", value={}, label="Weblog Usage Stats" }) local res, err = pcall(function() databaseconnect(DatabaseUser) retval.value = listusagestats() or {} databasedisconnect() end) if not res then retval.errtxt = err end return retval end function getauditstats() local result = {} result.auditstart = cfe({ value=config.auditstart or "", label="Audit Start Time" }) result.auditend = cfe({ value=config.auditend or "", label="Audit End Time" }) result.groupby = cfe({ value=config.groupby or "clientuserid", label="Group By" }) result.stats = cfe({ type="list", value={}, label="Audit Block Statistics" }) local res, err = pcall(function() if config.auditstart ~= "" and config.auditend ~= "" then databaseconnect(DatabaseUser) result.stats.value = groupflaggedlogentries(config.auditstart, config.auditend, result.groupby.value) or {} databasedisconnect() end end) return cfe({ type="group", value=result, errtxt=err, label="Weblog Audit Statistics" }) end function completeaudit(timestamp) local conf = getconfig() conf.value.auditstart.value = conf.value.auditend.value local now = os.time() conf.value.auditend.value = timestamp or os.date("%Y-%m-%d %H:%M:%S", now - now%86400 - 86400) conf = updateconfig(conf) local retval = cfe({ value="Audit completed", label="Complete Audit Result" }) if conf.errtxt then retval.value = "" retval.errtxt = "Failed to complete audit\n"..conf.errtxt.."\n"..conf.value.auditend.errtxt end return retval end function getconfig() local result = {} result.auditstart = cfe({ value=config.auditstart or "", label="Audit Start Time", seq=1 }) result.auditend = cfe({ value=config.auditend or "", label="Audit End Time", seq=2 }) result.groupby = cfe({ type="select", value=config.groupby or "clientuserid", label="Group results by", option={"clientuserid", "clientip"}, descr="Display audit results based on user ID or IP", seq=3 }) result.score = cfe({ value=config.score or "0", label="Minimum Score", descr="Default minimum Score to search for", seq=4 }) result.sortby = cfe({ type="select", value=config.sortby or "logdatetime", label="Sort By field", option={"logdatetime", "logdatetime DESC", "clientuserid", "clientuserid DESC", "clientip", "clientip DESC", "bytes", "bytes DESC", "score", "score DESC", "reason"}, descr="Default sort order", seq=5 }) result.badyesno = cfe({ type="boolean", value=(config.badyesno == "true"), label="Display Suspect Records", descr="By default, only show records flagged as suspect", seq=6 }) result.deniedyesno = cfe({ type="boolean", value=(config.deniedyesno == "true"), label="Display Denied Records", descr="By default, only show records with denied URI", seq=7 }) result.bypassyesno = cfe({ type="boolean", value=(config.bypassyesno == "true"), label="Display Bypass Records", descr="By default, only show records with bypass attempts", seq=8 }) result.shorturi = cfe({ type="boolean", value=(config.shorturi == "true"), label="Truncate URLs", descr="You can limit the length of displayed URLs by enabling this option", seq=9 }) result.shortreason = cfe({ type="boolean", value=(config.shortreason == "true"), label="Short Reason", descr="Display a short reason (dansguardian only)", seq=10 }) result.window = cfe({ value=config.window or "5", label="Time Window", descr="Minutes of activity to display before and after selected block", seq=11 }) result.purgedays = cfe({ value=config.purgedays or "30", label="Days before Purge", descr="Days to keep full history, regardless of audit", seq=12 }) result.stoponerror = cfe({ type="boolean", value=(config.stoponerror == "true"), label="Stop on Error", descr="Stop import of logs if an error is encountered", seq=13}) return cfe({ type="group", value=result, label="Weblog Config" }) end local function validateconfig(newconfig) local success = modelfunctions.validateselect(newconfig.value.groupby) success = modelfunctions.validateselect(newconfig.value.sortby) and success if not validator.is_integer(newconfig.value.score.value) then newconfig.value.score.errtxt = "Must be a number" success = false end if newconfig.value.window.value == "" then newconfig.value.window.errtxt = "Cannot be blank" success = false elseif not validator.is_integer(newconfig.value.window.value) then newconfig.value.window.errtxt = "Must be a number" success = false end if not validator.is_integer(newconfig.value.purgedays.value) then newconfig.value.purgedays.errtxt = "Must be a number" success = false end local res, err = pcall(function() databaseconnect(DatabaseUser) local s if newconfig.value.auditstart.value ~= "" then s,newconfig.value.auditstart.errtxt,newconfig.value.auditstart.value = convertdatabaseentry("TIMESTAMP", newconfig.value.auditstart.value) success = success and s end if newconfig.value.auditend.value ~= "" then s,newconfig.value.auditend.errtxt,newconfig.value.auditend.value = convertdatabaseentry("TIMESTAMP", newconfig.value.auditend.value) success = success and s end databasedisconnect() end) if not res and err then newconfig.value.auditstart.errtxt = err newconfig.value.auditend.errtxt = err success = false end return success, newconfig end function updateconfig(newconfig) local success = validateconfig(newconfig) if success then configcontent = format.update_ini_file(configcontent, "", "auditstart", newconfig.value.auditstart.value) configcontent = format.update_ini_file(configcontent, "", "auditend", newconfig.value.auditend.value) configcontent = format.update_ini_file(configcontent, "", "window", newconfig.value.window.value) configcontent = format.update_ini_file(configcontent, "", "purgedays", newconfig.value.purgedays.value) configcontent = format.update_ini_file(configcontent, "", "groupby", newconfig.value.groupby.value) configcontent = format.update_ini_file(configcontent, "", "shorturi", tostring(newconfig.value.shorturi.value)) configcontent = format.update_ini_file(configcontent, "", "shortreason", tostring(newconfig.value.shortreason.value)) configcontent = format.update_ini_file(configcontent, "", "stoponerror", tostring(newconfig.value.stoponerror.value)) configcontent = format.update_ini_file(configcontent, "", "badyesno", tostring(newconfig.value.badyesno.value)) configcontent = format.update_ini_file(configcontent, "", "deniedyesno", tostring(newconfig.value.deniedyesno.value)) configcontent = format.update_ini_file(configcontent, "", "bypassyesno", tostring(newconfig.value.bypassyesno.value)) configcontent = format.update_ini_file(configcontent, "", "score", tostring(newconfig.value.score.value)) configcontent = format.update_ini_file(configcontent, "", "sortby", tostring(newconfig.value.sortby.value)) fs.write_file(configfile, configcontent) config = format.parse_ini_file(configcontent, "") or {} else newconfig.errtxt = "Failed to update config" end return newconfig end function getnewadhocquery() local query = {} query.query = cfe({ label="Query select statement" }) return cfe({ type="group", value=query, label="Ad-hoc Query" }) end function adhocquery(query) local success = true query.value.query.value = query.value.query.value:lower() if query.value.query.value == "" then query.value.query.errtxt = "Empty select statement" success = false elseif not string.find(query.value.query.value, "^%s*select%s") then query.value.query.errtxt = "Must be a select statement" success = false end if success then local cur local res, err = pcall(function() databaseconnect(DatabaseUser) cur = assert (con:execute(query.value.query.value)) databasedisconnect() end) if not res or not cur then query.value.query.errtxt = err or "Select failed" query.errtxt = "Query failed" else query.value.result = cfe({ type="list", value={}, label="Select result" }) local result = query.value.result.value local row = cur:fetch ({}, "a") while row do result[#result+1] = {} for name,val in pairs(row) do result[#result][name] = val end row = cur:fetch (row, "a") end -- close everything cur:close() end else query.errtxt = "Query failed" end return query end function testdatabase() local retval = cfe({ type="boolean", value=false, label="Database present" }) local dbs = listdatabases() for i,db in ipairs(dbs) do if db == DatabaseName then retval.value = true break end end return retval end function getnewdatabase() local database = {} local errtxt database.password = cfe({ label="Password" }) database.password_confirm = cfe({ label="Password (confirm)" }) local test = testdatabase() if test.value then errtxt = "Database already exists!" success = false end return cfe({ type="group", value=database, label="Create Database", errtxt=errtxt }) end function create_database(database) local success = true local errtxt if database.value.password.value == "" or string.match(database.value.password.value, "'%s") then database.value.password.errtxt = "Invalid password" success = false end if database.value.password.value ~= database.value.password_confirm.value then database.value.password_confirm.errtxt = "Password does not match" success = false end local test = testdatabase() if test.value then errtxt = "Database already exists!" success = false end if success then errtxt = createdatabase(database.value.password.value) test = testdatabase() if not test.value then success = false else local res, err = pcall(function() databaseconnect(DatabaseOwner, database.value.password.value) for i,scr in ipairs(database_creation_script) do assert (con:execute(scr)) end databasedisconnect() -- put the password in the config file for future use configcontent = format.update_ini_file(configcontent, "", "password", database.value.password.value) fs.write_file(configfile, configcontent) config = format.parse_ini_file(configcontent, "") or {} end) if not res then errtxt = err success = false end end if not success then deletedatabase() end end if not success then database.errtxt = "Failed to create database" if errtxt then database.errtxt = database.errtxt.."\n"..errtxt end end return database end function listfiles() local retval = cfe({ type="list", value={}, label="Weblog Files" }) for i,file in ipairs(files) do local details = fs.stat(file) or {} details.filename = file retval.value[#retval.value + 1] = details end return retval end function readfile(filename) return modelfunctions.getfiledetails(filename, files) end function updatefile(filedetails) return modelfunctions.setfiledetails(filedetails, files) end