mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
feat: Add security.txt support
This commit adds support for the security.txt file, including the necessary configuration files and templates. The security.txt file provides information about security policies and contact details for reporting vulnerabilities.
This commit is contained in:
parent
3540903f47
commit
33ec069780
5 changed files with 329 additions and 1 deletions
|
|
@ -5,7 +5,8 @@
|
|||
"blacklist",
|
||||
"greylist",
|
||||
"bunkernet",
|
||||
"limit"
|
||||
"limit",
|
||||
"securitytxt"
|
||||
],
|
||||
"init_worker": [
|
||||
"redis",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
{% if USE_SECURITYTXT == "yes" and SECURITYTXT_CONTACT != "" +%}
|
||||
location = {{ SECURITYTXT_URI }} {
|
||||
default_type 'text/plain; charset=utf-8';
|
||||
root /usr/share/bunkerweb/core/securitytxt/templates;
|
||||
content_by_lua_block {
|
||||
local logger = require "bunkerweb.logger":new("SECURITYTXT")
|
||||
local helpers = require "bunkerweb.helpers"
|
||||
|
||||
local ngx = ngx
|
||||
local ERR = ngx.ERR
|
||||
local INFO = ngx.INFO
|
||||
local fill_ctx = helpers.fill_ctx
|
||||
local save_ctx = helpers.save_ctx
|
||||
local tostring = tostring
|
||||
|
||||
local ok, ret, errors, ctx = fill_ctx()
|
||||
if not ok then
|
||||
logger:log(ERR, "fill_ctx() failed : " .. ret)
|
||||
elseif errors then
|
||||
for i, error in ipairs(errors) do
|
||||
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
|
||||
end
|
||||
end
|
||||
local securitytxt = require "securitytxt.securitytxt":new(ctx)
|
||||
local ret = securitytxt:content()
|
||||
if not ret.ret then
|
||||
logger:log(ERR, "securitytxt:content() failed : " .. ret.msg)
|
||||
else
|
||||
logger:log(INFO, "securitytxt:content() success : " .. ret.msg)
|
||||
end
|
||||
save_ctx(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
location = /security.txt {
|
||||
return 301 {{ SECURITYTXT_URI }};
|
||||
}
|
||||
{% endif %}
|
||||
115
src/common/core/securitytxt/plugin.json
Normal file
115
src/common/core/securitytxt/plugin.json
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"id": "securitytxt",
|
||||
"name": "Security.txt",
|
||||
"description": "Manage the security.txt file. A proposed standard which allows websites to define security policies.",
|
||||
"version": "1.0",
|
||||
"stream": "yes",
|
||||
"settings": {
|
||||
"USE_SECURITYTXT": {
|
||||
"context": "multisite",
|
||||
"default": "no",
|
||||
"help": "Enable security.txt file.",
|
||||
"id": "use-securitytxt",
|
||||
"label": "Use security.txt",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"SECURITYTXT_URI": {
|
||||
"context": "multisite",
|
||||
"default": "/.well-known/security.txt",
|
||||
"help": "Indicates the URI where the \"security.txt\" file will be accessible from.",
|
||||
"id": "securitytxt-uri",
|
||||
"label": "Security.txt URI",
|
||||
"regex": "^\\/\\S*$",
|
||||
"type": "text"
|
||||
},
|
||||
"SECURITYTXT_CONTACT": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates a method that researchers should use for reporting security vulnerabilities such as an email address, a phone number, and/or a web page with contact information. (If the value is empty, the security.txt file will not be created as it is a required field)",
|
||||
"id": "securitytxt-contact",
|
||||
"label": "Security.txt contact",
|
||||
"regex": "^((mailto:|tel:|https:\\/\\/)\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-contact"
|
||||
},
|
||||
"SECURITYTXT_EXPIRES": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates the date and time after which the data contained in the \"security.txt\" file is considered stale and should not be used (If the value is empty, the value will always be the current date and time + 1 year).",
|
||||
"id": "securitytxt-expiration",
|
||||
"label": "Security.txt expiration",
|
||||
"regex": "^([0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9]))?$",
|
||||
"type": "text"
|
||||
},
|
||||
"SECURITYTXT_ENCRYPTION": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates an encryption key that security researchers should use for encrypted communication.",
|
||||
"id": "securitytxt-encryption",
|
||||
"label": "Security.txt encryption",
|
||||
"regex": "^((https:\\/\\/|dns:|openpgp4fpr:)\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-encryption"
|
||||
},
|
||||
"SECURITYTXT_ACKNOWLEDGEMENTS": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates a link to a page where security researchers are recognized for their reports.",
|
||||
"id": "securitytxt-acknowledgement",
|
||||
"label": "Security.txt acknowledgement",
|
||||
"regex": "^(https:\\/\\/\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-acknowledgement"
|
||||
},
|
||||
"SECURITYTXT_PREFERRED_LANG": {
|
||||
"context": "multisite",
|
||||
"default": "en",
|
||||
"help": "Can be used to indicate a set of natural languages that are preferred when submitting security reports.",
|
||||
"id": "securitytxt-preferred-lang",
|
||||
"label": "Security.txt preferred language",
|
||||
"regex": "^[a-zA-Z]{2,3}(-[a-zA-Z]{2})?(, [a-zA-Z]{2,3}(-[a-zA-Z]{2})?)*$",
|
||||
"type": "text"
|
||||
},
|
||||
"SECURITYTXT_CANONICAL": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates the canonical URIs where the \"security.txt\" file is located, which is usually something like \"https://example.com/.well-known/security.txt\". (If the value is empty, the default value will be automatically generated from the site URL + SECURITYTXT_URI)",
|
||||
"id": "securitytxt-canonical",
|
||||
"label": "Security.txt canonical",
|
||||
"regex": "^(https:\\/\\/\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-canonical"
|
||||
},
|
||||
"SECURITYTXT_POLICY": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Indicates a link to where the vulnerability disclosure policy is located.",
|
||||
"id": "securitytxt-policy",
|
||||
"label": "Security.txt policy",
|
||||
"regex": "^(https:\\/\\/\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-policy"
|
||||
},
|
||||
"SECURITYTXT_HIRING": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "Used for linking to the vendor's security-related job positions.",
|
||||
"id": "securitytxt-hiring",
|
||||
"label": "Security.txt hiring",
|
||||
"regex": "^(https:\\/\\/\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-hiring"
|
||||
},
|
||||
"SECURITYTXT_CSAF": {
|
||||
"context": "multisite",
|
||||
"default": "",
|
||||
"help": "A link to the provider-metadata.json of your CSAF (Common Security Advisory Framework) provider.",
|
||||
"id": "securitytxt-csaf",
|
||||
"label": "Security.txt CSAF",
|
||||
"regex": "^(https:\\/\\/\\S+)?$",
|
||||
"type": "text",
|
||||
"multiple": "securitytxt-csaf"
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/common/core/securitytxt/securitytxt.lua
Normal file
147
src/common/core/securitytxt/securitytxt.lua
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
local class = require "middleclass"
|
||||
local plugin = require "bunkerweb.plugin"
|
||||
local utils = require "bunkerweb.utils"
|
||||
|
||||
local ngx = ngx
|
||||
local ERR = ngx.ERR
|
||||
local get_phase = ngx.get_phase
|
||||
local subsystem = ngx.config.subsystem
|
||||
local get_multiple_variables = utils.get_multiple_variables
|
||||
|
||||
local template
|
||||
local render = nil
|
||||
if subsystem == "http" then
|
||||
template = require "resty.template"
|
||||
render = template.render
|
||||
end
|
||||
|
||||
local securitytxt = class("securitytxt", plugin)
|
||||
|
||||
function securitytxt:initialize(ctx)
|
||||
-- Call parent initialize
|
||||
plugin.initialize(self, "securitytxt", ctx)
|
||||
-- Load data from datastore if needed
|
||||
if get_phase() ~= "init" then
|
||||
-- Get security policies from datastore
|
||||
local security_policies, err = self.datastore:get("plugin_securitytxt_security_policies", true)
|
||||
if not security_policies then
|
||||
self.logger:log(ERR, err)
|
||||
return
|
||||
end
|
||||
self.security_policies = {
|
||||
["uri"] = "",
|
||||
["acknowledgements"] = {},
|
||||
["canonical"] = {},
|
||||
["contact"] = {},
|
||||
["encryption"] = {},
|
||||
["expires"] = "",
|
||||
["hiring"] = {},
|
||||
["policy"] = {},
|
||||
["preferred_lang"] = "",
|
||||
["csaf"] = {},
|
||||
}
|
||||
-- Extract global security policies
|
||||
if security_policies.global then
|
||||
for k, v in pairs(security_policies.global) do
|
||||
self.security_policies[k] = v
|
||||
end
|
||||
end
|
||||
-- Extract and overwrite if needed server security policies
|
||||
if security_policies[self.ctx.bw.server_name] then
|
||||
for k, v in pairs(security_policies[self.ctx.bw.server_name]) do
|
||||
self.security_policies[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function securitytxt:init()
|
||||
-- Get variables
|
||||
local variables, err = get_multiple_variables({
|
||||
"SECURITYTXT_URI",
|
||||
"SECURITYTXT_ACKNOWLEDGEMENTS",
|
||||
"SECURITYTXT_CANONICAL",
|
||||
"SECURITYTXT_CONTACT",
|
||||
"SECURITYTXT_ENCRYPTION",
|
||||
"SECURITYTXT_EXPIRES",
|
||||
"SECURITYTXT_HIRING",
|
||||
"SECURITYTXT_POLICY",
|
||||
"SECURITYTXT_PREFERRED_LANG",
|
||||
"SECURITYTXT_CSAF",
|
||||
})
|
||||
if variables == nil then
|
||||
return self:ret(false, err)
|
||||
end
|
||||
-- Store security policies name and value
|
||||
local data = {}
|
||||
local key
|
||||
for srv, vars in pairs(variables) do
|
||||
if data[srv] == nil then
|
||||
data[srv] = {
|
||||
["uri"] = "",
|
||||
["acknowledgements"] = {},
|
||||
["canonical"] = {},
|
||||
["contact"] = {},
|
||||
["encryption"] = {},
|
||||
["expires"] = "",
|
||||
["hiring"] = {},
|
||||
["policy"] = {},
|
||||
["preferred_lang"] = "",
|
||||
["csaf"] = {},
|
||||
}
|
||||
end
|
||||
for var, value in pairs(vars) do
|
||||
if value ~= "" then
|
||||
key = string.lower(string.gsub(string.gsub(var, "^SECURITYTXT_", ""), "_%d+$", ""))
|
||||
if key == "uri" or key == "expires" or key == "preferred_lang" then
|
||||
data[srv][key] = value
|
||||
else
|
||||
data[srv][key][#data[srv][key] + 1] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local ok
|
||||
ok, err = self.datastore:set("plugin_securitytxt_security_policies", data, nil, true)
|
||||
if not ok then
|
||||
return self:ret(false, err)
|
||||
end
|
||||
return self:ret(true, "successfully loaded security policies")
|
||||
end
|
||||
|
||||
function securitytxt:content()
|
||||
-- Check if content is needed
|
||||
if self.variables["USE_SECURITYTXT"] == "no" then
|
||||
return self:ret(true, "securitytxt not activated")
|
||||
end
|
||||
|
||||
-- Check if display content is needed
|
||||
if self.ctx.bw.uri ~= self.variables["SECURITYTXT_URI"] then
|
||||
return self:ret(true, "securitytxt not requested")
|
||||
end
|
||||
|
||||
-- Check if a contact key is set
|
||||
if self.security_policies["contact"] == nil or #self.security_policies["contact"] == 0 then
|
||||
return self:ret(false, "no contact key set")
|
||||
end
|
||||
|
||||
local data = {
|
||||
server_name = self.ctx.bw.server_name,
|
||||
scheme = self.ctx.bw.scheme,
|
||||
}
|
||||
|
||||
for k, v in pairs(self.security_policies) do
|
||||
data[k] = v
|
||||
end
|
||||
|
||||
-- If expires isn't set, set it to 1 year in the future and make it a ISO.8601-1 and ISO.8601-2 date
|
||||
if data["expires"] == "" then
|
||||
data["expires"] = os.date("%Y-%m-%dT%H:%M:%S", os.time() + 31536000)
|
||||
end
|
||||
|
||||
-- Render content
|
||||
render("security.txt", data)
|
||||
return self:ret(true, "content displayed")
|
||||
end
|
||||
|
||||
return securitytxt
|
||||
27
src/common/core/securitytxt/templates/security.txt
Normal file
27
src/common/core/securitytxt/templates/security.txt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{% for _, c in ipairs(contact) do %}
|
||||
Contact: {*c*}
|
||||
{% end %}
|
||||
Expires: {*expires*}
|
||||
{% for _, e in ipairs(encryption) do %}
|
||||
Encryption: {*e*}
|
||||
{% end %}
|
||||
{% for _, a in ipairs(acknowledgements) do %}
|
||||
Acknowledgements: {*a*}
|
||||
{% end %}
|
||||
Preferred-Languages: {*preferred_lang*}
|
||||
{% if canonical and #canonical > 0 then %}
|
||||
{% for _, ca in ipairs(canonical) do %}
|
||||
Canonical: {*ca*}
|
||||
{% end %}
|
||||
{% else %}
|
||||
Canonical: {*scheme*}://{*server_name*}{*uri*}
|
||||
{% end %}
|
||||
{% for _, p in ipairs(policy) do %}
|
||||
Policy: {*p*}
|
||||
{% end %}
|
||||
{% for _, h in ipairs(hiring) do %}
|
||||
Hiring: {*h*}
|
||||
{% end %}
|
||||
{% for _, cs in ipairs(csaf) do %}
|
||||
CSAF: {*cs*}
|
||||
{% end %}
|
||||
Loading…
Reference in a new issue