diff options
Diffstat (limited to 'nselib/ls.lua')
-rw-r--r-- | nselib/ls.lua | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/nselib/ls.lua b/nselib/ls.lua new file mode 100644 index 0000000..9d190ff --- /dev/null +++ b/nselib/ls.lua @@ -0,0 +1,368 @@ +--- +-- Report file and directory listings. +-- +-- For scripts that gather and report directory listings, this script provides +-- a common output format and helpful script arguments. +-- +-- The arguments can either be set for all the scripts using this +-- module (--script-args ls.arg=value) or for one particular script +-- (--script-args afp-ls.arg=value). If both are specified for the +-- same argument, the script-specific value is used. +-- +-- @args ls.maxdepth The maximum depth to recurse into a directory. If less +-- than 0 (e.g. -1) then recursion is unlimited. +-- (default: 0, no recursion). +-- @args ls.maxfiles The maximum number of files to return. Set to 0 or less to +-- disable this limit. (default: 10). +-- @args ls.checksum (boolean) Download each file and calculate a +-- SHA1 checksum. Although this is a module +-- argument, the implementation is done in each +-- script and is currently only supported by smb-ls +-- and http-ls +-- @args ls.errors (boolean) Report errors +-- @args ls.empty (boolean) Report empty volumes (with no information +-- or error) +-- @args ls.human (boolean) Show file sizes in human-readable format with K, +-- M, G, T, P suffixes. Some services return human-readable +-- sizes natively; in these cases, the size is reported as +-- given. +-- +-- @author Pierre Lalet <pierre@droids-corp.org> +-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html +----------------------------------------------------------------------- + +local LIBRARY_NAME = "ls" + +local math = require "math" +local stdnse = require "stdnse" +local string = require "string" +local tab = require "tab" +local table = require "table" + +_ENV = stdnse.module("ls", stdnse.seeall) + +local config_values = { + ["maxdepth"] = 1, + ["maxfiles"] = 10, + ["checksum"] = false, + ["errors"] = false, + ["empty"] = false, + ["human"] = false, +} + +--- Convert an argument to its expected type +local function convert_arg(argval, argtype) + if argtype == "number" then + return tonumber(argval) + elseif argtype == "boolean" then + if argval == "false" or argval == "no" or argval == "0" then + return false + else + return true + end + end + return argval +end + +--- Update config_values using module arguments ("ls.argname", as +-- opposed to script-specific arguments, "http-ls.argname") +for argname, argvalue in pairs(config_values) do + local argval = stdnse.get_script_args(LIBRARY_NAME .. "." .. argname) + if argval ~= nil then + config_values[argname] = convert_arg(argval, type(argvalue)) + end +end + +--- Get a config value from (by order or priority): +-- 1. a script-specific argument (e.g., http-ls.argname) +-- 2. a module argument (ls.argname) +-- 3. the default value +-- @param argname The name of the configuration parameter +-- @return The configuration value +function config(argname) + local argval = stdnse.get_script_args(stdnse.getid() .. "." .. argname) + if argval == nil then + return config_values[argname] + else + return convert_arg(argval, type(config_values[argname])) + end +end + +--- Create a new script output. +-- @return The ls output object to be passed to other functions +function new_listing() + local output = stdnse.output_table() + output['curvol'] = nil + output['volumes'] = {} + output['errors'] = {} + output['info'] = {} + output['total'] = { + ['files'] = 0, + ['bytes'] = 0, + } + return output +end + +--- Create a new volume within the provided output +-- @param output The ls output object, from new_listing() +-- @param name The name of the volume +-- @param hasperms Boolean true if the volume listing will include permissions +function new_vol(output, name, hasperms) + local curvol = stdnse.output_table() + local files = tab.new() + local i = 1 + if hasperms then + tab.add(files, 1, "PERMISSION") + tab.add(files, 2, "UID") + tab.add(files, 3, "GID") + i = 4 + end + tab.add(files, i, "SIZE") + tab.add(files, i + 1, "TIME") + tab.add(files, i + 2, "FILENAME") + if config("checksum") then + tab.add(files, i + 3, "CHECKSUM") + end + tab.nextrow(files) + curvol['name'] = name + curvol['files'] = files + curvol['count'] = 0 + curvol['bytes'] = 0 + curvol['errors'] = {} + curvol['info'] = {} + curvol['hasperms'] = hasperms + output['curvol'] = curvol +end + +--- Report an error, using stdnse.debug() and (depending on the +-- configuration settings) adding the error message to the output. +-- @param output The ls output object, from new_listing() +-- @param err The error message to report +-- @param level The debug level (default: 1) +function report_error(output, err, level) + level = level or 1 + if output["curvol"] == nil then + stdnse.debug(level, string.format("error: %s", err)) + else + stdnse.debug(level, string.format("error [%s]: %s", + output["curvol"]["name"], err)) + end + if config('errors') then + if output["curvol"] == nil then + table.insert(output["errors"], err) + else + table.insert(output["curvol"]["errors"], err) + end + end +end + +--- Report information, using stdnse.debug() and adding the message +-- to the output. +-- @param output The ls output object, from new_listing() +-- @param info The info message to report +-- @param level The debug level (default: 1) +function report_info(output, info, level) + level = level or 1 + if output["curvol"] == nil then + stdnse.debug(level, string.format("info: %s", info)) + table.insert(output["info"], info) + else + stdnse.debug(level, string.format("info [%s]: %s", + output["curvol"]["name"], info)) + table.insert(output["curvol"]["info"], info) + end +end + +local units = { + ["k"] = 1024, + ["m"] = 1024^2, + ["g"] = 1024^3, + ["t"] = 1024^4, + ["p"] = 1024^5, +} + +--- Get a size as an integer from a (possibly) human readable input. +local function get_size(size) + local bsize = tonumber(size) + if bsize == nil then + local unit = string.lower(string.sub(size, -1, -1)) + bsize = tonumber(string.sub(size, 0, -2)) + if units[unit] ~= nil and bsize ~= nil then + bsize = bsize * units[unit] + local sigfigs = #(string.match(size, "[0-9.]+")) + if string.find(size, "%.") then + sigfigs = sigfigs - 1 + end + local d = math.ceil(math.log(bsize, 10)) + local power = sigfigs - d + local magnitude = 10^power + local shifted = math.floor(bsize * magnitude) + bsize = math.floor(shifted / magnitude) + else + bsize = nil + end + end + return bsize +end + +--- Add a new file to the current volume. +-- @param output The ls output object, from new_listing() +-- @param file A table containing the information about the file +-- @return Boolean true if the script may continue adding files, false if +-- maxfiles has been reached. +function add_file(output, file) + local curvol = output.curvol + local files = curvol["files"] + for i, info in ipairs(file) do + tab.add(files, i, tostring(info)) + end + local size = get_size(file[curvol.hasperms and 4 or 1]) + if size then + curvol["bytes"] = curvol["bytes"] + size + end + tab.nextrow(files) + curvol["count"] = curvol["count"] + 1 + return (config("maxfiles") == 0 or config("maxfiles") == nil + or config("maxfiles") > curvol["count"]) +end + +--- Close the current volume. It is mandatory to call this function +-- before calling new_vol() again or before calling end_listing(). +-- @param output The ls output object, from new_listing() +function end_vol(output) + local curvol = output.curvol + local vol = {["volume"] = curvol["name"]} + local empty = true + -- "files" is a tab.lua table, so row 1 is the table heading + if #curvol["files"] ~= 1 then + vol["files"] = curvol["files"] + empty = false + end + if #curvol["errors"] ~= 0 then + vol["errors"] = curvol["errors"] + empty = false + end + if #curvol["info"] ~= 0 then + vol["info"] = curvol["info"] + empty = false + end + if not empty or config("empty") then + table.insert(output["volumes"], vol) + end + output["total"]["files"] = output["total"]["files"] + curvol["count"] + output["total"]["bytes"] = output["total"]["bytes"] + curvol["bytes"] + output["curvol"] = nil +end + +--- Convert a files table to structured data. +local function files_to_structured(files) + local result = {} + local fields = table.remove(files, 1) + for i=1, #fields do + fields[i] = string.lower(fields[i]) + end + for i, file in ipairs(files) do + result[i] = {} + for j, value in ipairs(file) do + result[i][fields[j]] = value + end + end + return result +end + +--- Convert a files table to human readable data. +local function files_to_readable(files) + local outtab = tab.new() + local fields = files[1] + local isize + tab.addrow(outtab, table.unpack(fields)) + for i, field in ipairs(fields) do + if string.lower(field) == "size" then + isize = i + break + end + end + local units = {"K", "M", "G", "T", "P"} + for i = 2, #files do + local outfile = {} + for j, value in ipairs(files[i]) do + outfile[j] = value + end + if config("human") then + local size = tonumber(outfile[isize]) + -- If tonumber didn't work, it's already in human-readable format + if size ~= nil then + local iunit = 0 + while size > 1024 and units[iunit+1] do + size = size / 1024 + iunit = iunit + 1 + end + if units[iunit] then + outfile[isize] = string.format("%.1f %s", size, units[iunit]) + else + outfile[isize] = tostring(size) + end + end + end + tab.addrow(outtab, table.unpack(outfile)) + end + return tab.dump(outtab) +end + +--- Close current listing. Return both the structured and the human +-- readable outputs. +-- @param output The ls output object, from new_listing() +-- @return Structured output +-- @return Human readable output +function end_listing(output) + assert(output["curvol"] == nil) + local text = {} + local empty = true + if #output["info"] == 0 then + output["info"] = nil + else + for _, line in ipairs(output["info"]) do + text[#text + 1] = line + end + empty = false + end + if #output["errors"] == 0 then + output["errors"] = nil + else + for _, line in ipairs(output["errors"]) do + text[#text + 1] = string.format("ERROR: %s", line) + end + empty = false + end + if #output["volumes"] == 0 then + output["volumes"] = nil + output["total"] = nil + else + for _, volume in ipairs(output["volumes"]) do + text[#text + 1] = string.format("Volume %s", volume["volume"]) + if volume["info"] then + for _, line in ipairs(volume["info"]) do + text[#text + 1] = string.format(" %s", line) + end + end + if volume["errors"] then + for _, line in ipairs(volume["errors"]) do + text[#text + 1] = string.format(" ERROR: %s", line) + end + end + if volume["files"] then + text[#text + 1] = files_to_readable(volume["files"]) + volume["files"] = files_to_structured(volume["files"]) + end + text[#text + 1] = "" + end + empty = false + end + if empty then + return nil + else + return output, table.concat(text, "\n") + end +end + +return _ENV |