#!/usr/bin/env luajit local args = {...} --[[ TODO - What to do about HTTPS://deb.devuan.org/ redirects. Some mirrors give a 404. Sledjhamr gives a 404, coz it's not listening on 443 for deb.devuan.org. Some mirrors give a 200. They shouldn't have the proper certificate, but are giving a result anyway. ]] origin = false verbosity = -1 keep = false -- TODO - Should actually implement this. fork = true options = { referenceSite = { typ = "string", help = "", value = "pkgmaster.devuan.org", }, roundRobin = { typ = "string", help = "", value = "deb.devuan.org", }, tests = { typ = "table", help = "", value = { "IPv4", "IPv6", -- "ftp", "http", "https", -- "rsync", -- "DNS-RR", "Protocol", -- "URL-Sanity", -- "Integrity", -- "Updated", }, }, reports = { typ = "table", help = "", value = { "DNS", "email", -- "Nagios", -- "Prometheus", -- "RRD", "web", }, }, } local defaultURL = {scheme = "http"} local downloadLock = "flock -n results/wget-" local download = "wget --timeout=300 -np -N -r -P results " -- Note wget has a default read timeout of 900 seconds (15 minutes). local releases = {"jessie", "ascii", "beowulf", "ceres"} local releaseFiles = { -- Release file. "/Release", "/InRelease", "/main/binary-all/Packages.gz", -- Contents files. "/main/Contents-all.gz", "/main/Contents-amd64.gz", "/main/Contents-arm64.gz", "-security/main/Contents-all.gz", "-security/main/Contents-amd64.gz", "-security/main/Contents-arm64.gz", } local notExist = { "ceres-security" -- This will never exist, it's our code name for the testing suite. } local referenceDebs = { -- Debian package. "merged/pool/DEBIAN/main/d/dash/dash_0.5.8-2.4_amd64.deb", -- Debian security package. NOTE this one should always be redirected? "merged/pool/DEBIAN-SECURITY/updates/main/a/apt/apt-transport-https_1.4.9_amd64.deb", } local referenceDevs = { -- Devuan package. NOTE this one should not get redirected, but that's more a warning than an error. "merged/pool/DEVUAN/main/d/desktop-base/desktop-base_2.0.3_all.deb", -- "merged/pool/DEVUAN/main/u/util-linux/util-linux_2.32.1-0.1+devuan2.1_amd64.deb", } local arg = {} local sendArgs = "" local logFile local socket = require 'socket' local ftp = require 'socket.ftp' local http = require 'socket.http' local url = require 'socket.url' -- Use this to dump a table to a string. dumpTable = function (table, space, name) local r = "" if "" == space then r = r .. space .. name .. " =\n" else r = r .. space .. "[" .. name .. "] =\n" end r = r .. space .. "{\n" r = r .. dumpTableSub(table, space .. " ") if "" == space then r = r .. space .. "}\n" else r = r .. space .. "},\n" end return r end dumpTableSub = function (table, space) local r = "" for k, v in pairs(table) do if type(k) == "string" then k = '"' .. k .. '"' end if type(v) == "table" then r = r .. dumpTable(v, space, k) elseif type(v) == "string" then r = r .. space .. "[" .. k .. "] = '" .. v .. "';\n" elseif type(v) == "function" then r = r .. space .. "[" .. k .. "] = function ();\n" elseif type(v) == "userdata" then r = r .. space .. "userdata " .. "[" .. k .. "];\n" elseif type(v) == "boolean" then if (v) then r = r .. space .. "[" .. k .. "] = true;\n" else r = r .. space .. "[" .. k .. "] = false;\n" end else r = r .. space .. "[" .. k .. "] = " .. v .. ";\n" end end return r end local ip = "" local results = {} local logPre = function() if nil ~= logFile then logFile:write("\n") logFile:write("\n") end end local logPost = function() if nil ~= logFile then logFile:write(" \n") end end local log = function(v, t, s, prot, test, host) local x = "" if nil == prot then prot = "" end if nil == test then test = "" end x = x .. prot if "" ~= test then if #x > 0 then x = x .. " " end x = x .. test end if nil ~= host then if #x > 0 then x = x .. " " end x = x .. host end if #x > 0 then t = t .. "(" .. x .. ")" if "" ~= prot then if "" == test then if nil == results[prot] then results[prot] = {errors = 0; warnings = 0} end if v == 0 then results[prot].errors = results[prot].errors + 1 end if v == 1 then results[prot].warnings = results[prot].warnings + 1 end else if nil == results[prot] then results[prot] = {errors = 0; warnings = 0} end if nil == results[prot][test] then results[prot][test] = {errors = 0; warnings = 0} end if v == 0 then results[prot][test].errors = results[prot][test].errors + 1 end if v == 1 then results[prot][test].warnings = results[prot][test].warnings + 1 end end end end if v <= verbosity then if 3 <= verbosity then t = os.date() .. " " .. t end print(t .. ": " .. s) end if nil ~= logFile then local colour = "white" if -1 == v then colour = "fuchsia" end -- CRITICAL if 0 == v then colour = "red " end -- ERROR if 1 == v then colour = "yellow " end -- WARNING if 2 == v then colour = "white " end -- INFO if 3 == v then colour = "gray " end -- DEBUG logFile:write(os.date() .. " " .. t .. ": " .. s .. "
\n") logFile:flush() end end local D = function(s) log(3, "DEBUG ", s) end local I = function(s) log(2, "INFO ", s) end local W = function(s, p, t, h) log(1, "WARNING ", s, p, t, h) end local E = function(s, p, t, h) log(0, "ERROR ", s, p, t, h) end local C = function(s) log(-1, "CRITICAL", s) end local mirrors = {} local testing = function(t, host) for i, v in pairs(options.tests.value) do if t == v then local h = mirrors[host] if nil == h then return true end if true == h["Protocols"][t] then return true else D("Skipping " .. t .. " checks for " .. host) end end end return false end local execute = function (s) D(" executing " .. s) --[[ Damn os.execute() Lua 5.1 says it returns "a status code, which is system-dependent" Lua 5.2 says it returns true/nil, "exit"/"signal", the status code. I'm getting 7168 or 0. No idea what the fuck that is. local ok, rslt, status = os.execute(s) ]] local f = io.popen(s .. ' ; echo "$?"', 'r') local status = "" local result = "" -- The last line will be the command's returned status, collect everything else in result. for l in f:lines() do result = result .. status .. "\n" status = l end return 0 + status, result end local fork = function(s) D(" executing " .. s) os.execute(s .. " &") end local checkExes = function (exe) local count = io.popen('ps x | grep "' .. exe .. '" | grep -v " grep " | wc -l'):read("*l") D(count .. " " .. exe .. " commands still running.") return tonumber(count) end local repoExists = function (r) r = r:match("([%a-]*)") if nil == r then return false end for k, v in pairs(notExist) do if v == r then return false end end return true end local IP = {} gatherIPs = function (host) if nil == IP[host] then local IPs local dig = io.popen('dig +keepopen +noall +nottlid +answer ' .. host .. ' A ' .. host .. ' AAAA ' .. host .. ' CNAME ' .. host .. ' SRV | sort -r | uniq') repeat IPs = dig:read("*l") if nil ~= IPs then for k, t, v in IPs:gmatch("([%w_%-%.]*)%.%s*IN%s*(%a*)%s*(.*)") do if "." == v:sub(-1, -1) then v = v:sub(1, -2) end if nil == IP[k] then IP[k] = {} end IP[k][v] = t D(" DNS record " .. host .. " == " .. k .. " type " .. t .. " -> " .. v) if t == "CNAME" then gatherIPs(v) IP[k][v] = IP[v] elseif v == "SRV" then print("SVR record found, now what do we do?") end end end until nil == IPs end end -- Returns FTP directory listing local nlst = function (u) local t = {} local p = url.parse(u) p.command = "nlst" p.sink = ltn12.sink.table(t) local r, e = ftp.get(p) return r and table.concat(t), e end local timeouts = 0; checkHEAD = function (host, URL, r, retry) if nil == r then r = 0 end if nil == retry then retry = 0 end local check = "Checking file" local PU = url.parse(URL, defaultURL) local pu = url.parse(PU.scheme .. "://" .. host, defaultURL) if 0 < r then check = "Redirecting to" end if 0 < retry then os.execute("sleep " .. math.random(1, 4)) check = "Retry " .. retry .. " " .. check end if 2 <= timeouts then E("too many timeouts! " .. check .. " " .. host .. " -> " .. URL, PU.scheme, "", host) return end if 20 <= r then E("too many redirects! " .. check .. " " .. host .. " -> " .. URL, PU.scheme, "", host) return end if 4 <= retry then E("too many retries! " .. check .. " " .. host .. " -> " .. URL, PU.scheme, "", host) return end D(PU.scheme .. " :// " .. check .. " " .. host .. " -> " .. URL) if not testing(PU.scheme, host) then D("Not testing " .. PU.scheme .. " " .. host .. " -> " .. URL); return end -- TODO - Perhaps we should try it anyway, and mark it as a warning if it DOES work? if "https" == PU.scheme and options.roundRobin.value == host then D("Not testing " .. PU.scheme .. " " .. host .. " -> " .. URL .. " mirrors shouldn't have the correct cert."); return end --[[ Using curl command line - -I - HEAD --connect-to IP - connect to IP, but use SNI from URL. -header "" - add extra headers. -L - DO follow redirects. --max-redirs n - set maximum redirects, default is 50, -1 = unlimited. --retry n - maximum retries, default is 0, no retries. -o file - write to file instead of stdout. --path-as-is - https://curl.haxx.se/libcurl/c/CURLOPT_PATH_AS_IS.html might be useful for URL_sanity. -s silent - don't output progress or error messages. --connect-timeout n - timeout in seconds. Should return with error code 28 on a timeout? -D file - write the received headers to a file. This includes the status code and string. ]] local fname = host .. "_" .. PU.host .. "_" .. PU.path:gsub("/", "_") .. ".txt" local hdr = "" local IP = "" if pu.host ~= PU.host then if "http" == PU.scheme then hdr = '-H "Host: ' .. host .. '"' end IP = '--connect-to ' .. PU.host end local cmd = 'curl -I --retry 0 -s --path-as-is --connect-timeout 15 --max-redirs 0 ' .. IP .. ' ' .. '-o /dev/null -D results/"HEADERS_' .. fname .. '" ' .. hdr .. ' -w "#%{http_code} %{ssl_verify_result} %{url_effective}\\n" ' .. PU.scheme .. '://' .. host .. PU.path .. ' >>results/"STATUS_' .. fname .. '"' local status, result = execute(cmd) os.execute('cat results/"HEADERS_' .. fname .. '" >>results/"STATUS_' .. fname .. '" 2>/dev/null; rm results/"HEADERS_' .. fname .. '" 2>/dev/null') if 28 == status then E(" TIMEOUT " .. timeouts + 1 .. ", retry " .. retry + 1, PU.scheme, "", host) timeouts = timeouts + 1 checkHEAD(host, URL, r, retry + 1, timeouts) return elseif 0 ~= status then E(" The curl command return an error code of " .. status .. ", consult the curl manual for what this means.", PU.scheme, "", host) checkHEAD(host, URL, r, retry + 1, timeouts) return end local rfile, e = io.open("results/STATUS_" .. fname, "r") local code = "000" local cstr = "" local location = nil if nil == rfile then W("opening results/STATUS_" .. fname .. " file - " .. e) else for line in rfile:lines("*l") do if "#" == line:sub(1, 1) then code = line:sub(2, 4) if ("https" == PU.scheme) and ("0" ~= line:sub(6, 6)) then E(" The certificate is invalid.", PU.scheme, "https", host) end elseif "http" == line:sub(1, 4):lower() then -- -2 coz the headers file gets a \r at the end. cstr = line:sub(14, -2) elseif "location" == line:sub(1, 8):lower() then location = line:sub(11, -2) end end end os.execute('rm results/"STATUS_' .. fname .. '" 2>/dev/null') if ("4" == tostring(code):sub(1, 1)) or ("5" == tostring(code):sub(1, 1)) then E(" " .. code .. " " .. cstr .. ". " .. check .. " " .. host .. " -> " .. URL, PU.scheme, "", host) else I(" " .. code .. " " .. cstr .. ". " .. check .. " " .. host .. " -> " .. URL) timeouts = timeouts - 1 -- Backoff the timeouts count if we managed to get through. if nil ~= location then pu = url.parse(location, defaultURL) if (pu.scheme ~= PU.scheme) then if testing("Protocol") then W(" protocol changed during redirect! " .. check .. " " .. host .. " -> " .. URL .. " -> " .. location, PU.scheme, "Protocol", host) end if (pu.host == host) and pu.path == PU.path then D("Not testing protocol change " .. URL .. " -> " .. location); return end end if location == URL then E(" redirect loop! " .. check .. " " .. host .. " -> " .. URL, PU.scheme, "", host) elseif nil == pu.host then I(" relative redirect. " .. check .. " " .. host .. " -> " .. URL .. " -> " .. location) checkHEAD(host, PU.scheme .. "://" .. PU.host .. location, r + 1) elseif (PU.host == pu.host) or (host == pu.host) then checkHEAD(pu.host, location, r + 1) else --[[ The hard part here is that we end up throwing ALL of the test files at the redirected location. Not good for deb.debian.org, which we should only be throwing .debs at. What we do is loop through the DNS entries, and only test the specific protocol & file being tested here. This is what I came up with for checking if we are already testing a specific URL. Still duplicates a tiny bit, but much less than the previous find based method. ]] local file = pu.host .. "://" .. pu.path local f = io.popen(string.format('if [ ! -f results/%s.check ] ; then touch results/%s.check; echo -n "check"; fi', file:gsub("/", "_"), file:gsub("/", "_") )):read("*a") if (nil == f) or ("check" == f) then I(" Now checking redirected host " .. file) checkHost(pu.host, pu.host, nil, "redir", pu.path) else D(" Already checking " .. file) end end end end end local checkFiles = function (host, ip, path, file) if nil == path then path = "" end if nil ~= file then if "redir" == ip then ip = host end I(" Checking IP for file " .. host .. " -> " .. ip .. " " .. path .. " " .. file) if testing("http", host) then checkHEAD(host, "http://" .. ip .. path .. "/" .. file) end if testing("https", host) then checkHEAD(host, "https://" .. ip .. path .. "/" .. file) end else I(" Checking IP " .. host .. " -> " .. ip .. " " .. path) for i, s in pairs(referenceDevs) do local t = timeouts; timeouts = 0 if testing("http", host) then checkHEAD(host, "http://" .. ip .. path .. "/" .. s) end t = t + timeouts; timeouts = 0 if testing("https", host) then checkHEAD(host, "https://" .. ip .. path .. "/" .. s) end if 4 <= (t + timeouts) then return end end for i, s in pairs(releases) do for j, k in pairs(releaseFiles) do if repoExists(s .. k) then local t = timeouts; timeouts = 0 if testing("http", host) then checkHEAD(host, "http://" .. ip .. path .. "/merged/dists/" .. s .. k) end t = t + timeouts; timeouts = 0 if testing("https", host) then checkHEAD(host, "https://" .. ip .. path .. "/merged/dists/" .. s .. k) end if 4 <= (t + timeouts) then return end end end end end end checkHost = function (orig, host, path, ip, file) if nil == host then host = orig end if nil == path then path = "" end if nil == file then file = "" end local ph = url.parse("http://" .. host) if (nil ~= ip) and ("redir" ~= ip) then local po = url.parse("http://" .. orig) if "" ~= file then D("checking redirected file " .. po.host .. " " .. file) checkFiles(po.host, ip, path, file) else checkFiles(po.host, ip, path) end else if orig == host then D("checkHost " .. orig .. "" .. file) if testing("IPv4") then fork("ionice -c3 ./apt-panopticon.lua " .. sendArgs .. " -o " .. orig .. path .. " " .. file) end else D("checkHost " .. orig .. " -> " .. host) end local h = mirrors[ph.host] if nil == h then return end for k, v in pairs(h.IPs) do if "table" == type(v) then for k1, v1 in pairs(v) do if v1 == "A" then if testing("IPv4") then fork("ionice -c3 ./apt-panopticon.lua " .. sendArgs .. " " .. orig .. path .. " " .. k1 .. " " .. file) end elseif v1 == "AAAA" then if testing("IPv6") then fork("ionice -c3 ./apt-panopticon.lua " .. sendArgs .. " " .. orig .. path .. " [" .. k1 .. "] " .. file) end end end else if v == "A" then if testing("IPv4") then fork("ionice -c3 ./apt-panopticon.lua " .. sendArgs .. " " .. orig .. path .. " " .. k .. " " .. file) end elseif v == "AAAA" then if testing("IPv6") then fork("ionice -c3 ./apt-panopticon.lua " .. sendArgs .. " " .. orig .. path .. " [" .. k .. "] " .. file) end end end end end end local downloads = function (cut, host, URL) if 0 ~= cut then cd = " --cut-dirs=" .. cut .. " " else cd = "" end if nil == URL then URL = "/" end local lock = "%s-" .. host .. ".log " local log = " --rejected-log=results/wget-%s_REJECTS-" .. host .. ".log -a results/wget-%s-" .. host .. ".log " I("starting file download commands for " .. host .. " " .. URL) local cm = "ionice -c3 " .. downloadLock .. lock:format("debs") .. download .. log:format("debs", "debs") .. cd for i, s in pairs(referenceDevs) do cm = cm .. " https://" .. host .. URL .. "/" .. s end for i, s in pairs(referenceDebs) do cm = cm .. " https://" .. host .. URL .. "/" .. s end for i, s in pairs(releases) do fork(cm) cm = "ionice -c3 " .. downloadLock .. lock:format(s) .. download .. log:format(s, s) .. cd if repoExists(s .. k) then for j, k in pairs(releaseFiles) do cm = cm .. " https://" .. host .. URL .. "/merged/dists/" .. s .. k end end end fork(cm) end local getMirrors = function () local mirrors = {} local host = "" local m = {} local active = true local URL = "https://" .. options.referenceSite.value .. "/mirror_list.txt" I("getting mirrors.") local p, c, h = http.request(URL) if nil == p then E(c .. " fetching " .. URL) else for l in p:gmatch("\n*([^\n]+)\n*") do local t, d = l:match("(%a*):%s*(.*)") d = string.lower(d) if "FQDN" == t then if "" ~= host then if active then mirrors[host] = m end m = {} active = true end host = d m[t] = d gatherIPs(host) m["IPs"] = IP[host] elseif "Protocols" == t then local prot = {} for w in d:gmatch("(%w+)") do prot[w] = true; end m[t] = prot elseif "Active" == t and nil == d:find("yes", 1, true) then W("Mirror " .. host .. " is not active - " .. d, "", "", host) active = false -- TODO - Should do some input validation on BaseURL, and everything else. else m[t] = d end end if "" ~= host and active then mirrors[host] = m end end mirrors[options.roundRobin.value] = { ["Protocols"] = { ["http"] = true; ["https"] = true; }; ["FQDN"] = 'deb.devuan.org'; ["Active"] = 'yes'; ["BaseURL"] = 'deb.devuan.org'; } gatherIPs(options.roundRobin.value) mirrors[options.roundRobin.value].IPs = IP[options.roundRobin.value] local file, e = io.open("results/mirrors.lua", "w+") if nil == file then C("opening mirrors file - " .. e) else file:write(dumpTable(mirrors, "", "mirrors") .. "\nreturn mirrors\n") file:close() end return mirrors end if 0 ~= #args then local option = "" for i, a in pairs(args) do if ("--help" == a) or ("-h" == a) then print("I should write some docs, huh? Read README.md for instructions.") elseif "--version" == a then print("apt-panopticon version 0.1 WIP development version") elseif "-v" == a then verbosity = verbosity + 1 sendArgs = sendArgs .. a .. " " elseif "-q" == a then verbosity = -1 sendArgs = sendArgs .. a .. " " elseif "-k" == a then keep = true elseif "-n" == a then fork = false elseif "-o" == a then origin = true elseif "--" == a:sub(1, 2) then local s, e = a:find("=") if nil == s then e = -1 end option = a:sub(3, e - 1) local o = options[option] if nil == o then print("Unknown option --" .. option) option = "" else option = a sendArgs = sendArgs .. a .. " " local s, e = a:find("=") if nil == s then e = 0 end option = a:sub(3, e - 1) if "table" == options[option].typ then local result = {} for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do local f = t:sub(1, 1) local n = t:sub(2, -1) if ("+" ~= f) and ("-" ~= f) then table.insert(result, t) end end if 0 ~= #result then options[option].value = result else for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do local f = t:sub(1, 1) local n = t:sub(2, -1) if "+" == f then table.insert(options[option].value, n) elseif "-" == f then local r = {} for i, k in pairs(options[option].value) do if k ~= n then table.insert(r, k) end end options[option].value = r end end end else options[option].value = a end option = "" end elseif "-" == a:sub(1, 1) then print("Unknown option " .. a) else table.insert(arg, a) end end end --print(dumpTable(options.tests.value, "", "tests")) execute("mkdir -p results") if 0 < #arg then if "/" == arg[1]:sub(-1, -1) then W("slash at end of path! " .. arg[1]) arg[1] = arg[1]:sub(1, -2) end if " " == arg[1]:sub(-1, -1) then W("space at end of path! " .. arg[1]) arg[1] = arg[1]:sub(1, -2) end local pu = url.parse("http://" .. arg[1]) if nil ~= arg[2] then logFile, e = io.open("results/LOG_" .. pu.host .. "_" .. arg[2] .. ".html", "a+") else logFile, e = io.open("results/LOG_" .. pu.host .. ".html", "a+") end if nil == logFile then C("opening log file - " .. e); return end logPre() I("Starting tests for " ..arg[1] .. " with these tests - " .. table.concat(options.tests.value, ", ")) mirrors = loadfile("results/mirrors.lua")() if nil ~= arg[2] then I(" Using IP " .. arg[2]); ip = arg[2] end if nil ~= arg[3] then I(" Using file " .. arg[3]); end for k, v in pairs{"ftp", "http", "https", "rsync"} do if testing(v) then local tests = {errors = 0; warnings = 0} if testing("Integrity") then tests.Integrity = {errors = 0; warnings = 0} end if testing("Protocol") then tests.Protocol = {errors = 0; warnings = 0} end if testing("Updated") then tests.Updated = {errors = 0; warnings = 0} end if testing("URL-Sanity") then tests.URL_Sanity = {errors = 0; warnings = 0} end results[v] = tests end end if testing("Integrity") or testing("Updated") then if nil == arg[3] then if not keep then execute("rm -fr results/" .. pu.host) end cut = 0 for t in arg[1]:gmatch("(/)") do cut = cut + 1 end downloads(cut, pu.host, pu.path) checkExes("apt-panopticon.lua " .. sendArgs) checkExes(downloadLock) end end if origin then checkFiles(pu.host, pu.host, pu.path); else checkHost(pu.host, pu.host, pu.path, arg[2], arg[3]) end logPost() logFile:close() local f = pu.host if "" ~= ip then f = f .. "_" .. ip end local rfile, e = io.open("results/" .. f .. ".lua", "w+") if nil == rfile then C("opening results file - " .. e) else rfile:write(dumpTable(results, "", "results") .. "\nreturn results\n") rfile:close() end else if not keep then os.execute("rm -f results/*.log") os.execute("rm -f results/*.html") os.execute("rm -f results/*.txt") end os.execute("rm -f results/*.check") os.execute("mkdir -p results; touch results/stamp") logFile, e = io.open("results/LOG_apt-panopticon.html", "a+") if nil == logFile then C("opening log file - " .. e); return end logPre() I("Starting tests " .. table.concat(options.tests.value, ", ")) execute("mkdir -p results") mirrors = getMirrors() checkHost(options.referenceSite.value) for k, m in pairs(mirrors) do if "/" == m.BaseURL:sub(-1, -1) then W("slash at end of BaseURL in mirror_list.txt! " .. m.BaseURL, "", "", m.FQDN) m.BaseURL = m.BaseURL:sub(1, -2) end if " " == m.BaseURL:sub(-1, -1) then W("space at end of BaseURL in mirror_list.txt! " .. m.BaseURL, "", "", m.FQDN) m.BaseURL = m.BaseURL:sub(1, -2) end local pu = url.parse("http://" .. m.BaseURL) if options.referenceSite.value ~= pu.host then checkHost(m.BaseURL) checkExes("apt-panopticon.lua " .. sendArgs) if testing("Integrity") or testing("Updated") then checkExes(downloadLock) end end end while 1 <= checkExes("apt-panopticon.lua " .. sendArgs) do os.execute("sleep 10") end if testing("Integrity") or testing("Updated") then while 0 < checkExes(downloadLock) do os.execute("sleep 10") end end os.execute("rm -f results/*.check") -- Create the reports. for n, r in pairs(options.reports.value) do local report = "apt-panopticon-report-" .. r .. ".lua" local rfile, e = io.open(report, "r") if nil == rfile then C("opening " .. report .. " file - " .. e) else rfile:close() I("Creating " .. report .. " report.") execute("./" .. report .. " ") end end logPost() logFile:close() end