Compare commits

...

5 Commits

Author SHA1 Message Date
Leonardo Grasso
7ab327749f chore(userspace/engine): format lua source code
Signed-off-by: Leonardo Grasso <me@leonardograsso.com>
2020-07-29 14:44:55 +02:00
Leonardo Grasso
4450fd3c4c revert(rules): remove require_engine_version at rule level
Signed-off-by: Leonardo Grasso <me@leonardograsso.com>
2020-07-29 14:44:54 +02:00
Lorenzo Fontana
5cca1a6589 rule(Create Disallowed Pod): required_engine_version 5
rule(Create Privileged Pod): required_engine_version 5
rule(Create Sensitive Mount Pod): required_engine_version 5
rule(Create HostNetwork Pod): required_engine_version 5
rule(Pod Created in Kube Namespace): required_engine_version 5
rule(ClusterRole With Wildcard Created): required_engine_version 5
rule(ClusterRole With Write Privileges Created): required_engine_version 5
rule(ClusterRole With Pod Exec Created): required_engine_version 5

Co-Authored-By: Leonardo Di Donato <leodidonato@gmail.com>
Signed-off-by: Lorenzo Fontana <lo@linux.com>
2020-07-29 14:44:51 +02:00
Lorenzo Fontana
130126f170 rules(Container Drift Detected (open+create)): specify that rule is only
compatible with engine 6

Co-Authored-By: Leonardo Di Donato <leodidonato@gmail.com>
Signed-off-by: Lorenzo Fontana <lo@linux.com>
2020-07-29 14:43:26 +02:00
Lorenzo Fontana
c886debf83 rules: the required_engine_version is now on by default
Co-Authored-By: Leonardo Di Donato <leodidonato@gmail.com>
Signed-off-by: Lorenzo Fontana <lo@linux.com>
2020-07-29 14:42:05 +02:00
7 changed files with 1990 additions and 1410 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -44,10 +44,18 @@
items: ["vpa-recommender", "vpa-updater"] items: ["vpa-recommender", "vpa-updater"]
- list: allowed_k8s_users - list: allowed_k8s_users
items: [ items:
"minikube", "minikube-user", "kubelet", "kops", "admin", "kube", "kube-proxy", "kube-apiserver-healthcheck", [
"kubernetes-admin", "minikube",
vertical_pod_autoscaler_users, "minikube-user",
"kubelet",
"kops",
"admin",
"kube",
"kube-proxy",
"kube-apiserver-healthcheck",
"kubernetes-admin",
vertical_pod_autoscaler_users,
] ]
- rule: Disallowed K8s User - rule: Disallowed K8s User
@@ -115,6 +123,7 @@
- macro: health_endpoint - macro: health_endpoint
condition: ka.uri=/healthz condition: ka.uri=/healthz
# requires FALCO_ENGINE_VERSION 5
- rule: Create Disallowed Pod - rule: Create Disallowed Pod
desc: > desc: >
Detect an attempt to start a pod with a container image outside of a list of allowed images. Detect an attempt to start a pod with a container image outside of a list of allowed images.
@@ -124,6 +133,7 @@
source: k8s_audit source: k8s_audit
tags: [k8s] tags: [k8s]
# requires FALCO_ENGINE_VERSION 5
- rule: Create Privileged Pod - rule: Create Privileged Pod
desc: > desc: >
Detect an attempt to start a pod with a privileged container Detect an attempt to start a pod with a privileged container
@@ -136,7 +146,8 @@
- macro: sensitive_vol_mount - macro: sensitive_vol_mount
condition: > condition: >
(ka.req.pod.volumes.hostpath intersects (/proc, /var/run/docker.sock, /, /etc, /root, /var/run/crio/crio.sock, /home/admin, /var/lib/kubelet, /var/lib/kubelet/pki, /etc/kubernetes, /etc/kubernetes/manifests)) (ka.req.pod.volumes.hostpath intersects (/proc, /var/run/docker.sock, /, /etc, /root, /var/run/crio/crio.sock, /home/admin, /var/lib/kubelet, /var/lib/kubelet/pki, /etc/kubernetes, /etc/kubernetes/manifests))
# requires FALCO_ENGINE_VERSION 5
- rule: Create Sensitive Mount Pod - rule: Create Sensitive Mount Pod
desc: > desc: >
Detect an attempt to start a pod with a volume from a sensitive host directory (i.e. /proc). Detect an attempt to start a pod with a volume from a sensitive host directory (i.e. /proc).
@@ -148,6 +159,7 @@
tags: [k8s] tags: [k8s]
# Corresponds to K8s CIS Benchmark 1.7.4 # Corresponds to K8s CIS Benchmark 1.7.4
# requires FALCO_ENGINE_VERSION 5
- rule: Create HostNetwork Pod - rule: Create HostNetwork Pod
desc: Detect an attempt to start a pod using the host network. desc: Detect an attempt to start a pod using the host network.
condition: kevt and pod and kcreate and ka.req.pod.host_network intersects (true) and not ka.req.pod.containers.image.repository in (falco_hostnetwork_images) condition: kevt and pod and kcreate and ka.req.pod.host_network intersects (true) and not ka.req.pod.containers.image.repository in (falco_hostnetwork_images)
@@ -179,7 +191,7 @@
- rule: Create/Modify Configmap With Private Credentials - rule: Create/Modify Configmap With Private Credentials
desc: > desc: >
Detect creating/modifying a configmap containing a private credential (aws key, password, etc.) Detect creating/modifying a configmap containing a private credential (aws key, password, etc.)
condition: kevt and configmap and kmodify and contains_private_credentials condition: kevt and configmap and kmodify and contains_private_credentials
output: K8s configmap with private credential (user=%ka.user.name verb=%ka.verb configmap=%ka.req.configmap.name config=%ka.req.configmap.obj) output: K8s configmap with private credential (user=%ka.user.name verb=%ka.verb configmap=%ka.req.configmap.name config=%ka.req.configmap.obj)
priority: WARNING priority: WARNING
@@ -236,6 +248,7 @@
condition: (ka.req.pod.containers.image.repository in (user_trusted_image_list)) condition: (ka.req.pod.containers.image.repository in (user_trusted_image_list))
# Detect any new pod created in the kube-system namespace # Detect any new pod created in the kube-system namespace
# requires FALCO_ENGINE_VERSION 5
- rule: Pod Created in Kube Namespace - rule: Pod Created in Kube Namespace
desc: Detect any attempt to create a pod in the kube-system or kube-public namespaces desc: Detect any attempt to create a pod in the kube-system or kube-public namespaces
condition: kevt and pod and kcreate and ka.target.namespace in (kube-system, kube-public) and not trusted_pod condition: kevt and pod and kcreate and ka.target.namespace in (kube-system, kube-public) and not trusted_pod
@@ -280,6 +293,7 @@
source: k8s_audit source: k8s_audit
tags: [k8s] tags: [k8s]
# requires FALCO_ENGINE_VERSION 5
- rule: ClusterRole With Wildcard Created - rule: ClusterRole With Wildcard Created
desc: Detect any attempt to create a Role/ClusterRole with wildcard resources or verbs desc: Detect any attempt to create a Role/ClusterRole with wildcard resources or verbs
condition: kevt and (role or clusterrole) and kcreate and (ka.req.role.rules.resources intersects ("*") or ka.req.role.rules.verbs intersects ("*")) condition: kevt and (role or clusterrole) and kcreate and (ka.req.role.rules.resources intersects ("*") or ka.req.role.rules.verbs intersects ("*"))
@@ -292,6 +306,7 @@
condition: > condition: >
(ka.req.role.rules.verbs intersects (create, update, patch, delete, deletecollection)) (ka.req.role.rules.verbs intersects (create, update, patch, delete, deletecollection))
# requires FALCO_ENGINE_VERSION 5
- rule: ClusterRole With Write Privileges Created - rule: ClusterRole With Write Privileges Created
desc: Detect any attempt to create a Role/ClusterRole that can perform write-related actions desc: Detect any attempt to create a Role/ClusterRole that can perform write-related actions
condition: kevt and (role or clusterrole) and kcreate and writable_verbs condition: kevt and (role or clusterrole) and kcreate and writable_verbs
@@ -300,6 +315,7 @@
source: k8s_audit source: k8s_audit
tags: [k8s] tags: [k8s]
# requires FALCO_ENGINE_VERSION 5
- rule: ClusterRole With Pod Exec Created - rule: ClusterRole With Pod Exec Created
desc: Detect any attempt to create a Role/ClusterRole that can exec to pods desc: Detect any attempt to create a Role/ClusterRole that can exec to pods
condition: kevt and (role or clusterrole) and kcreate and ka.req.role.rules.resources intersects ("pods/exec") condition: kevt and (role or clusterrole) and kcreate and ka.req.role.rules.resources intersects ("pods/exec")
@@ -463,20 +479,26 @@
source: k8s_audit source: k8s_audit
tags: [k8s] tags: [k8s]
# This macro disables following rule, change to k8s_audit_never_true to enable it # This macro disables following rule, change to k8s_audit_never_true to enable it
- macro: allowed_full_admin_users - macro: allowed_full_admin_users
condition: (k8s_audit_always_true) condition: (k8s_audit_always_true)
# This list includes some of the default user names for an administrator in several K8s installations # This list includes some of the default user names for an administrator in several K8s installations
- list: full_admin_k8s_users - list: full_admin_k8s_users
items: ["admin", "kubernetes-admin", "kubernetes-admin@kubernetes", "kubernetes-admin@cluster.local", "minikube-user"] items:
[
"admin",
"kubernetes-admin",
"kubernetes-admin@kubernetes",
"kubernetes-admin@cluster.local",
"minikube-user",
]
# This rules detect an operation triggered by an user name that is # This rules detect an operation triggered by an user name that is
# included in the list of those that are default administrators upon # included in the list of those that are default administrators upon
# cluster creation. This may signify a permission setting too broader. # cluster creation. This may signify a permission setting too broader.
# As we can't check for role of the user on a general ka.* event, this # As we can't check for role of the user on a general ka.* event, this
# may or may not be an administrator. Customize the full_admin_k8s_users # may or may not be an administrator. Customize the full_admin_k8s_users
# list to your needs, and activate at your discrection. # list to your needs, and activate at your discrection.
# # How to test: # # How to test:
@@ -526,7 +548,7 @@
output: > output: >
K8s Ingress Without TLS Cert Created (user=%ka.user.name ingress=%ka.target.name K8s Ingress Without TLS Cert Created (user=%ka.user.name ingress=%ka.target.name
namespace=%ka.target.namespace) namespace=%ka.target.namespace)
source: k8s_audit source: k8s_audit
priority: WARNING priority: WARNING
tags: [k8s, network] tags: [k8s, network]
@@ -572,4 +594,3 @@
priority: WARNING priority: WARNING
source: k8s_audit source: k8s_audit
tags: [k8s] tags: [k8s]

View File

@@ -1,3 +1,2 @@
- macro: allowed_k8s_containers - macro: allowed_k8s_containers
condition: (ka.req.pod.containers.image.repository in (nginx)) condition: (ka.req.pod.containers.image.repository in (nginx))

View File

@@ -1,4 +1,4 @@
-- Copyright (C) 2019 The Falco Authors. -- Copyright (C) 2020 The Falco Authors.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@@ -11,25 +11,24 @@
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and -- See the License for the specific language governing permissions and
-- limitations under the License. -- limitations under the License.
local parser = require("parser") local parser = require("parser")
local compiler = {} local compiler = {}
compiler.trim = parser.trim compiler.trim = parser.trim
function map(f, arr) function map(f, arr)
local res = {} local res = {}
for i,v in ipairs(arr) do for i, v in ipairs(arr) do
res[i] = f(v) res[i] = f(v)
end end
return res return res
end end
function foldr(f, acc, arr) function foldr(f, acc, arr)
for i,v in pairs(arr) do for i, v in pairs(arr) do
acc = f(acc, v) acc = f(acc, v)
end end
return acc return acc
end end
--[[ --[[
@@ -47,181 +46,192 @@ end
--]] --]]
function copy_ast_obj(obj) function copy_ast_obj(obj)
if type(obj) ~= 'table' then return obj end if type(obj) ~= 'table' then
local res = {} return obj
for k, v in pairs(obj) do res[copy_ast_obj(k)] = copy_ast_obj(v) end end
return res local res = {}
for k, v in pairs(obj) do
res[copy_ast_obj(k)] = copy_ast_obj(v)
end
return res
end end
function expand_macros(ast, defs, changed) function expand_macros(ast, defs, changed)
if (ast.type == "Rule") then if (ast.type == "Rule") then
return expand_macros(ast.filter, defs, changed) return expand_macros(ast.filter, defs, changed)
elseif ast.type == "Filter" then elseif ast.type == "Filter" then
if (ast.value.type == "Macro") then if (ast.value.type == "Macro") then
if (defs[ast.value.value] == nil) then if (defs[ast.value.value] == nil) then
return false, "Undefined macro '".. ast.value.value .. "' used in filter." return false, "Undefined macro '" .. ast.value.value .. "' used in filter."
end end
defs[ast.value.value].used = true defs[ast.value.value].used = true
ast.value = copy_ast_obj(defs[ast.value.value].ast) ast.value = copy_ast_obj(defs[ast.value.value].ast)
changed = true changed = true
return true, changed return true, changed
end end
return expand_macros(ast.value, defs, changed) return expand_macros(ast.value, defs, changed)
elseif ast.type == "BinaryBoolOp" then elseif ast.type == "BinaryBoolOp" then
if (ast.left.type == "Macro") then if (ast.left.type == "Macro") then
if (defs[ast.left.value] == nil) then if (defs[ast.left.value] == nil) then
return false, "Undefined macro '".. ast.left.value .. "' used in filter." return false, "Undefined macro '" .. ast.left.value .. "' used in filter."
end end
defs[ast.left.value].used = true defs[ast.left.value].used = true
ast.left = copy_ast_obj(defs[ast.left.value].ast) ast.left = copy_ast_obj(defs[ast.left.value].ast)
changed = true changed = true
end end
if (ast.right.type == "Macro") then if (ast.right.type == "Macro") then
if (defs[ast.right.value] == nil) then if (defs[ast.right.value] == nil) then
return false, "Undefined macro ".. ast.right.value .. " used in filter." return false, "Undefined macro " .. ast.right.value .. " used in filter."
end end
defs[ast.right.value].used = true defs[ast.right.value].used = true
ast.right = copy_ast_obj(defs[ast.right.value].ast) ast.right = copy_ast_obj(defs[ast.right.value].ast)
changed = true changed = true
end end
local status, changed_left = expand_macros(ast.left, defs, false) local status, changed_left = expand_macros(ast.left, defs, false)
if status == false then if status == false then
return false, changed_left return false, changed_left
end end
local status, changed_right = expand_macros(ast.right, defs, false) local status, changed_right = expand_macros(ast.right, defs, false)
if status == false then if status == false then
return false, changed_right return false, changed_right
end end
return true, changed or changed_left or changed_right return true, changed or changed_left or changed_right
elseif ast.type == "UnaryBoolOp" then elseif ast.type == "UnaryBoolOp" then
if (ast.argument.type == "Macro") then if (ast.argument.type == "Macro") then
if (defs[ast.argument.value] == nil) then if (defs[ast.argument.value] == nil) then
return false, "Undefined macro ".. ast.argument.value .. " used in filter." return false, "Undefined macro " .. ast.argument.value .. " used in filter."
end end
defs[ast.argument.value].used = true defs[ast.argument.value].used = true
ast.argument = copy_ast_obj(defs[ast.argument.value].ast) ast.argument = copy_ast_obj(defs[ast.argument.value].ast)
changed = true changed = true
end end
return expand_macros(ast.argument, defs, changed) return expand_macros(ast.argument, defs, changed)
end end
return true, changed return true, changed
end end
function get_macros(ast, set) function get_macros(ast, set)
if (ast.type == "Macro") then if (ast.type == "Macro") then
set[ast.value] = true set[ast.value] = true
return set return set
end end
if ast.type == "Filter" then if ast.type == "Filter" then
return get_macros(ast.value, set) return get_macros(ast.value, set)
end end
if ast.type == "BinaryBoolOp" then if ast.type == "BinaryBoolOp" then
local left = get_macros(ast.left, {}) local left = get_macros(ast.left, {})
local right = get_macros(ast.right, {}) local right = get_macros(ast.right, {})
for m, _ in pairs(left) do set[m] = true end for m, _ in pairs(left) do
for m, _ in pairs(right) do set[m] = true end set[m] = true
end
for m, _ in pairs(right) do
set[m] = true
end
return set return set
end end
if ast.type == "UnaryBoolOp" then if ast.type == "UnaryBoolOp" then
return get_macros(ast.argument, set) return get_macros(ast.argument, set)
end end
return set return set
end end
function get_filters(ast) function get_filters(ast)
local filters = {} local filters = {}
function cb(node) function cb(node)
if node.type == "FieldName" then if node.type == "FieldName" then
filters[node.value] = 1 filters[node.value] = 1
end end
end end
parser.traverse_ast(ast.filter.value, {FieldName=1} , cb) parser.traverse_ast(ast.filter.value, {
FieldName = 1
}, cb)
return filters return filters
end end
function compiler.expand_lists_in(source, list_defs) function compiler.expand_lists_in(source, list_defs)
for name, def in pairs(list_defs) do for name, def in pairs(list_defs) do
local bpos = string.find(source, name, 1, true) local bpos = string.find(source, name, 1, true)
while bpos ~= nil do while bpos ~= nil do
def.used = true def.used = true
local epos = bpos + string.len(name) local epos = bpos + string.len(name)
-- The characters surrounding the name must be delimiters of beginning/end of string -- The characters surrounding the name must be delimiters of beginning/end of string
if (bpos == 1 or string.match(string.sub(source, bpos-1, bpos-1), "[%s(),=]")) and (epos > string.len(source) or string.match(string.sub(source, epos, epos), "[%s(),=]")) then if (bpos == 1 or string.match(string.sub(source, bpos - 1, bpos - 1), "[%s(),=]")) and
new_source = "" (epos > string.len(source) or string.match(string.sub(source, epos, epos), "[%s(),=]")) then
new_source = ""
if bpos > 1 then if bpos > 1 then
new_source = new_source..string.sub(source, 1, bpos-1) new_source = new_source .. string.sub(source, 1, bpos - 1)
end end
sub = table.concat(def.items, ", ") sub = table.concat(def.items, ", ")
new_source = new_source..sub new_source = new_source .. sub
if epos <= string.len(source) then if epos <= string.len(source) then
new_source = new_source..string.sub(source, epos, string.len(source)) new_source = new_source .. string.sub(source, epos, string.len(source))
end end
source = new_source source = new_source
bpos = bpos + (string.len(sub)-string.len(name)) bpos = bpos + (string.len(sub) - string.len(name))
end end
bpos = string.find(source, name, bpos+1, true) bpos = string.find(source, name, bpos + 1, true)
end end
end end
return source return source
end end
function compiler.compile_macro(line, macro_defs, list_defs) function compiler.compile_macro(line, macro_defs, list_defs)
line = compiler.expand_lists_in(line, list_defs) line = compiler.expand_lists_in(line, list_defs)
local ast, error_msg = parser.parse_filter(line) local ast, error_msg = parser.parse_filter(line)
if (error_msg) then if (error_msg) then
msg = "Compilation error when compiling \""..line.."\": ".. error_msg msg = "Compilation error when compiling \"" .. line .. "\": " .. error_msg
return false, msg return false, msg
end end
-- Simply as a validation step, try to expand all macros in this -- Simply as a validation step, try to expand all macros in this
-- macro's condition. This changes the ast, so we make a copy -- macro's condition. This changes the ast, so we make a copy
-- first. -- first.
local ast_copy = copy_ast_obj(ast) local ast_copy = copy_ast_obj(ast)
if (ast.type == "Rule") then if (ast.type == "Rule") then
-- Line is a filter, so expand macro references -- Line is a filter, so expand macro references
repeat repeat
status, expanded = expand_macros(ast_copy, macro_defs, false) status, expanded = expand_macros(ast_copy, macro_defs, false)
if status == false then if status == false then
msg = "Compilation error when compiling \""..line.."\": ".. expanded msg = "Compilation error when compiling \"" .. line .. "\": " .. expanded
return false, msg return false, msg
end end
until expanded == false until expanded == false
else else
return false, "Unexpected top-level AST type: "..ast.type return false, "Unexpected top-level AST type: " .. ast.type
end end
return true, ast return true, ast
end end
--[[ --[[
@@ -229,32 +239,31 @@ end
--]] --]]
function compiler.compile_filter(name, source, macro_defs, list_defs) function compiler.compile_filter(name, source, macro_defs, list_defs)
source = compiler.expand_lists_in(source, list_defs) source = compiler.expand_lists_in(source, list_defs)
local ast, error_msg = parser.parse_filter(source) local ast, error_msg = parser.parse_filter(source)
if (error_msg) then if (error_msg) then
msg = "Compilation error when compiling \""..source.."\": "..error_msg msg = "Compilation error when compiling \"" .. source .. "\": " .. error_msg
return false, msg return false, msg
end end
if (ast.type == "Rule") then if (ast.type == "Rule") then
-- Line is a filter, so expand macro references -- Line is a filter, so expand macro references
repeat repeat
status, expanded = expand_macros(ast, macro_defs, false) status, expanded = expand_macros(ast, macro_defs, false)
if status == false then if status == false then
return false, expanded return false, expanded
end end
until expanded == false until expanded == false
else else
return false, "Unexpected top-level AST type: "..ast.type return false, "Unexpected top-level AST type: " .. ast.type
end end
filters = get_filters(ast) filters = get_filters(ast)
return true, ast, filters return true, ast, filters
end end
return compiler return compiler

View File

@@ -1,4 +1,4 @@
-- Copyright (C) 2019 The Falco Authors. -- Copyright (C) 2020 The Falco Authors.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@@ -12,7 +12,6 @@
-- See the License for the specific language governing permissions and -- See the License for the specific language governing permissions and
-- limitations under the License. -- limitations under the License.
-- --
--[[ --[[
Falco grammar and parser. Falco grammar and parser.
@@ -40,232 +39,258 @@ local space = lpeg.space
-- creates an error message for the input string -- creates an error message for the input string
local function syntaxerror(errorinfo, pos, msg) local function syntaxerror(errorinfo, pos, msg)
local error_msg = "%s: syntax error, %s" local error_msg = "%s: syntax error, %s"
return string.format(error_msg, pos, msg) return string.format(error_msg, pos, msg)
end end
-- gets the farthest failure position -- gets the farthest failure position
local function getffp(s, i, t) local function getffp(s, i, t)
return t.ffp or i, t return t.ffp or i, t
end end
-- gets the table that contains the error information -- gets the table that contains the error information
local function geterrorinfo() local function geterrorinfo()
return Cmt(Carg(1), getffp) * (C(V "OneWord") + Cc("EOF")) / function(t, u) return Cmt(Carg(1), getffp) * (C(V "OneWord") + Cc("EOF")) / function(t, u)
t.unexpected = u t.unexpected = u
return t return t
end end
end end
-- creates an errror message using the farthest failure position -- creates an errror message using the farthest failure position
local function errormsg() local function errormsg()
return geterrorinfo() / function(t) return geterrorinfo() / function(t)
local p = t.ffp or 1 local p = t.ffp or 1
local msg = "unexpected '%s', expecting %s" local msg = "unexpected '%s', expecting %s"
msg = string.format(msg, t.unexpected, t.expected) msg = string.format(msg, t.unexpected, t.expected)
return nil, syntaxerror(t, p, msg) return nil, syntaxerror(t, p, msg)
end end
end end
-- reports a syntactic error -- reports a syntactic error
local function report_error() local function report_error()
return errormsg() return errormsg()
end end
--- sets the farthest failure position and the expected tokens --- sets the farthest failure position and the expected tokens
local function setffp(s, i, t, n) local function setffp(s, i, t, n)
if not t.ffp or i > t.ffp then if not t.ffp or i > t.ffp then
t.ffp = i t.ffp = i
t.list = {} t.list = {}
t.list[n] = n t.list[n] = n
t.expected = "'" .. n .. "'" t.expected = "'" .. n .. "'"
elseif i == t.ffp then elseif i == t.ffp then
if not t.list[n] then if not t.list[n] then
t.list[n] = n t.list[n] = n
t.expected = "'" .. n .. "', " .. t.expected t.expected = "'" .. n .. "', " .. t.expected
end end
end end
return false return false
end end
local function updateffp(name) local function updateffp(name)
return Cmt(Carg(1) * Cc(name), setffp) return Cmt(Carg(1) * Cc(name), setffp)
end end
-- regular combinators and auxiliary functions -- regular combinators and auxiliary functions
local function token(pat, name) local function token(pat, name)
return pat * V "Skip" + updateffp(name) * P(false) return pat * V "Skip" + updateffp(name) * P(false)
end end
local function symb(str) local function symb(str)
return token(P(str), str) return token(P(str), str)
end end
local function kw(str) local function kw(str)
return token(P(str) * -V "idRest", str) return token(P(str) * -V "idRest", str)
end end
local function list(pat, sep) local function list(pat, sep)
return Ct(pat ^ -1 * (sep * pat ^ 0) ^ 0) / function(elements) return Ct(pat ^ -1 * (sep * pat ^ 0) ^ 0) / function(elements)
return {type = "List", elements = elements} return {
end type = "List",
elements = elements
}
end
end end
--http://lua-users.org/wiki/StringTrim -- http://lua-users.org/wiki/StringTrim
function trim(s) function trim(s)
if (type(s) ~= "string") then if (type(s) ~= "string") then
return s return s
end end
return (s:gsub("^%s*(.-)%s*$", "%1")) return (s:gsub("^%s*(.-)%s*$", "%1"))
end end
parser.trim = trim parser.trim = trim
local function terminal(tag) local function terminal(tag)
-- Rather than trim the whitespace in this way, it would be nicer to exclude it from the capture... -- Rather than trim the whitespace in this way, it would be nicer to exclude it from the capture...
return token(V(tag), tag) / function(tok) return token(V(tag), tag) / function(tok)
val = tok val = tok
if tag ~= "String" then if tag ~= "String" then
val = trim(tok) val = trim(tok)
end end
return {type = tag, value = val} return {
end type = tag,
value = val
}
end
end end
local function unaryboolop(op, e) local function unaryboolop(op, e)
return {type = "UnaryBoolOp", operator = op, argument = e} return {
type = "UnaryBoolOp",
operator = op,
argument = e
}
end end
local function unaryrelop(e, op) local function unaryrelop(e, op)
return {type = "UnaryRelOp", operator = op, argument = e} return {
type = "UnaryRelOp",
operator = op,
argument = e
}
end end
local function binaryop(e1, op, e2) local function binaryop(e1, op, e2)
if not op then if not op then
return e1 return e1
else else
return {type = "BinaryBoolOp", operator = op, left = e1, right = e2} return {
end type = "BinaryBoolOp",
operator = op,
left = e1,
right = e2
}
end
end end
local function bool(pat, sep) local function bool(pat, sep)
return Cf(pat * Cg(sep * pat) ^ 0, binaryop) return Cf(pat * Cg(sep * pat) ^ 0, binaryop)
end end
local function rel(left, sep, right) local function rel(left, sep, right)
return left * sep * right / function(e1, op, e2) return left * sep * right / function(e1, op, e2)
return {type = "BinaryRelOp", operator = op, left = e1, right = e2} return {
end type = "BinaryRelOp",
operator = op,
left = e1,
right = e2
}
end
end end
-- grammar -- grammar
local function filter(e) local function filter(e)
return {type = "Filter", value = e} return {
type = "Filter",
value = e
}
end end
local function rule(filter) local function rule(filter)
return {type = "Rule", filter = filter} return {
type = "Rule",
filter = filter
}
end end
local G = { local G = {
V "Start", -- Entry rule V "Start", -- Entry rule
Start = V "Skip" * (V "Comment" + V "Rule" / rule) ^ -1 * -1 + report_error(), Start = V "Skip" * (V "Comment" + V "Rule" / rule) ^ -1 * -1 + report_error(),
-- Grammar -- Grammar
Comment = P "#" * P(1) ^ 0, Comment = P "#" * P(1) ^ 0,
Rule = V "Filter" / filter * ((V "Skip") ^ -1), Rule = V "Filter" / filter * ((V "Skip") ^ -1),
Filter = V "OrExpression", Filter = V "OrExpression",
OrExpression = bool(V "AndExpression", V "OrOp"), OrExpression = bool(V "AndExpression", V "OrOp"),
AndExpression = bool(V "NotExpression", V "AndOp"), AndExpression = bool(V "NotExpression", V "AndOp"),
NotExpression = V "UnaryBoolOp" * V "NotExpression" / unaryboolop + V "ExistsExpression", NotExpression = V "UnaryBoolOp" * V "NotExpression" / unaryboolop + V "ExistsExpression",
ExistsExpression = terminal "FieldName" * V "ExistsOp" / unaryrelop + V "MacroExpression", ExistsExpression = terminal "FieldName" * V "ExistsOp" / unaryrelop + V "MacroExpression",
MacroExpression = terminal "Macro" + V "RelationalExpression", MacroExpression = terminal "Macro" + V "RelationalExpression",
RelationalExpression = rel(terminal "FieldName", V "RelOp", V "Value") + RelationalExpression = rel(terminal "FieldName", V "RelOp", V "Value") +
rel(terminal "FieldName", V "SetOp", V "InList") + rel(terminal "FieldName", V "SetOp", V "InList") + V "PrimaryExp",
V "PrimaryExp", PrimaryExp = symb("(") * V "Filter" * symb(")"),
PrimaryExp = symb("(") * V "Filter" * symb(")"), FuncArgs = symb("(") * list(V "Value", symb(",")) * symb(")"),
FuncArgs = symb("(") * list(V "Value", symb(",")) * symb(")"), -- Terminals
-- Terminals Value = terminal "Number" + terminal "String" + terminal "BareString",
Value = terminal "Number" + terminal "String" + terminal "BareString", InList = symb("(") * list(V "Value", symb(",")) * symb(")"),
InList = symb("(") * list(V "Value", symb(",")) * symb(")"), -- Lexemes
-- Lexemes Space = space ^ 1,
Space = space ^ 1, Skip = (V "Space") ^ 0,
Skip = (V "Space") ^ 0, idStart = alpha + P("_"),
idStart = alpha + P("_"), idRest = alnum + P("_"),
idRest = alnum + P("_"), Identifier = V "idStart" * V "idRest" ^ 0,
Identifier = V "idStart" * V "idRest" ^ 0, Macro = V "idStart" * V "idRest" ^ 0 * -P ".",
Macro = V "idStart" * V "idRest" ^ 0 * -P ".", Int = digit ^ 1,
Int = digit ^ 1, PathString = (alnum + S ",.-_/*?") ^ 1,
PathString = (alnum + S ",.-_/*?") ^ 1, PortRangeString = (V "Int" + S ":,") ^ 1,
PortRangeString = (V "Int" + S ":,") ^ 1, Index = V "PortRangeString" + V "Int" + V "PathString",
Index = V "PortRangeString" + V "Int" + V "PathString", FieldName = V "Identifier" * (P "." + V "Identifier") ^ 1 * (P "[" * V "Index" * P "]") ^ -1,
FieldName = V "Identifier" * (P "." + V "Identifier") ^ 1 * (P "[" * V "Index" * P "]") ^ -1, Name = C(V "Identifier") * -V "idRest",
Name = C(V "Identifier") * -V "idRest", Hex = (P("0x") + P("0X")) * xdigit ^ 1,
Hex = (P("0x") + P("0X")) * xdigit ^ 1, Expo = S("eE") * S("+-") ^ -1 * digit ^ 1,
Expo = S("eE") * S("+-") ^ -1 * digit ^ 1, Float = (((digit ^ 1 * P(".") * digit ^ 0) + (P(".") * digit ^ 1)) * V "Expo" ^ -1) + (digit ^ 1 * V "Expo"),
Float = (((digit ^ 1 * P(".") * digit ^ 0) + (P(".") * digit ^ 1)) * V "Expo" ^ -1) + (digit ^ 1 * V "Expo"), Number = C(V "Hex" + V "Float" + V "Int") / function(n)
Number = C(V "Hex" + V "Float" + V "Int") / function(n) return tonumber(n)
return tonumber(n) end,
end, String = (P '"' * C(((P "\\" * P(1)) + (P(1) - P '"')) ^ 0) * P '"' + P "'" *
String = (P '"' * C(((P "\\" * P(1)) + (P(1) - P '"')) ^ 0) * P '"' + C(((P "\\" * P(1)) + (P(1) - P "'")) ^ 0) * P "'"),
P "'" * C(((P "\\" * P(1)) + (P(1) - P "'")) ^ 0) * P "'"), BareString = C((P(1) - S " (),=") ^ 1),
BareString = C((P(1) - S " (),=") ^ 1), OrOp = kw("or") / "or",
OrOp = kw("or") / "or", AndOp = kw("and") / "and",
AndOp = kw("and") / "and", Colon = kw(":"),
Colon = kw(":"), RelOp = symb("=") / "=" + symb("==") / "==" + symb("!=") / "!=" + symb("<=") / "<=" + symb(">=") / ">=" + symb("<") /
RelOp = symb("=") / "=" + symb("==") / "==" + symb("!=") / "!=" + symb("<=") / "<=" + symb(">=") / ">=" + "<" + symb(">") / ">" + symb("contains") / "contains" + symb("icontains") / "icontains" + symb("glob") / "glob" +
symb("<") / "<" + symb("startswith") / "startswith" + symb("endswith") / "endswith",
symb(">") / ">" + SetOp = kw("in") / "in" + kw("intersects") / "intersects" + kw("pmatch") / "pmatch",
symb("contains") / "contains" + UnaryBoolOp = kw("not") / "not",
symb("icontains") / "icontains" + ExistsOp = kw("exists") / "exists",
symb("glob") / "glob" + -- for error reporting
symb("startswith") / "startswith" + OneWord = V "Name" + V "Number" + V "String" + P(1)
symb("endswith") / "endswith",
SetOp = kw("in") / "in" + kw("intersects") / "intersects" + kw("pmatch") / "pmatch",
UnaryBoolOp = kw("not") / "not",
ExistsOp = kw("exists") / "exists",
-- for error reporting
OneWord = V "Name" + V "Number" + V "String" + P(1)
} }
--[[ --[[
Parses a single filter and returns the AST. Parses a single filter and returns the AST.
--]] --]]
function parser.parse_filter(subject) function parser.parse_filter(subject)
local errorinfo = {subject = subject} local errorinfo = {
lpeg.setmaxstack(1000) subject = subject
local ast, error_msg = lpeg.match(G, subject, nil, errorinfo) }
return ast, error_msg lpeg.setmaxstack(1000)
local ast, error_msg = lpeg.match(G, subject, nil, errorinfo)
return ast, error_msg
end end
function print_ast(ast, level) function print_ast(ast, level)
local t = ast.type local t = ast.type
level = level or 0 level = level or 0
local prefix = string.rep(" ", level * 4) local prefix = string.rep(" ", level * 4)
level = level + 1 level = level + 1
if t == "Rule" then if t == "Rule" then
print_ast(ast.filter, level) print_ast(ast.filter, level)
elseif t == "Filter" then elseif t == "Filter" then
print_ast(ast.value, level) print_ast(ast.value, level)
elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then
print(prefix .. ast.operator) print(prefix .. ast.operator)
print_ast(ast.left, level) print_ast(ast.left, level)
print_ast(ast.right, level) print_ast(ast.right, level)
elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then
print(prefix .. ast.operator) print(prefix .. ast.operator)
print_ast(ast.argument, level) print_ast(ast.argument, level)
elseif t == "List" then elseif t == "List" then
for i, v in ipairs(ast.elements) do for i, v in ipairs(ast.elements) do
print_ast(v, level) print_ast(v, level)
end end
elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then
print(prefix .. t .. " " .. ast.value) print(prefix .. t .. " " .. ast.value)
elseif t == "MacroDef" then elseif t == "MacroDef" then
-- don't print for now -- don't print for now
else else
error("Unexpected type in print_ast: " .. t) error("Unexpected type in print_ast: " .. t)
end end
end end
parser.print_ast = print_ast parser.print_ast = print_ast
@@ -275,32 +300,32 @@ parser.print_ast = print_ast
-- cb(ast_node, ctx) -- cb(ast_node, ctx)
-- ctx is optional. -- ctx is optional.
function traverse_ast(ast, node_types, cb, ctx) function traverse_ast(ast, node_types, cb, ctx)
local t = ast.type local t = ast.type
if node_types[t] ~= nil then if node_types[t] ~= nil then
cb(ast, ctx) cb(ast, ctx)
end end
if t == "Rule" then if t == "Rule" then
traverse_ast(ast.filter, node_types, cb, ctx) traverse_ast(ast.filter, node_types, cb, ctx)
elseif t == "Filter" then elseif t == "Filter" then
traverse_ast(ast.value, node_types, cb, ctx) traverse_ast(ast.value, node_types, cb, ctx)
elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then elseif t == "BinaryBoolOp" or t == "BinaryRelOp" then
traverse_ast(ast.left, node_types, cb, ctx) traverse_ast(ast.left, node_types, cb, ctx)
traverse_ast(ast.right, node_types, cb, ctx) traverse_ast(ast.right, node_types, cb, ctx)
elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then elseif t == "UnaryRelOp" or t == "UnaryBoolOp" then
traverse_ast(ast.argument, node_types, cb, ctx) traverse_ast(ast.argument, node_types, cb, ctx)
elseif t == "List" then elseif t == "List" then
for i, v in ipairs(ast.elements) do for i, v in ipairs(ast.elements) do
traverse_ast(v, node_types, cb, ctx) traverse_ast(v, node_types, cb, ctx)
end end
elseif t == "MacroDef" then elseif t == "MacroDef" then
traverse_ast(ast.value, node_types, cb, ctx) traverse_ast(ast.value, node_types, cb, ctx)
elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then elseif t == "FieldName" or t == "Number" or t == "String" or t == "BareString" or t == "Macro" then
-- do nothing, no traversal needed -- do nothing, no traversal needed
else else
error("Unexpected type in traverse_ast: " .. t) error("Unexpected type in traverse_ast: " .. t)
end end
end end
parser.traverse_ast = traverse_ast parser.traverse_ast = traverse_ast

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
-- Copyright (C) 2019 The Falco Authors. -- Copyright (C) 2020 The Falco Authors.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@@ -12,55 +12,52 @@
-- See the License for the specific language governing permissions and -- See the License for the specific language governing permissions and
-- limitations under the License. -- limitations under the License.
-- --
local parser = require("parser") local parser = require("parser")
local sinsp_rule_utils = {} local sinsp_rule_utils = {}
function sinsp_rule_utils.check_for_ignored_syscalls_events(ast, filter_type, source) function sinsp_rule_utils.check_for_ignored_syscalls_events(ast, filter_type, source)
function check_syscall(val) function check_syscall(val)
if ignored_syscalls[val] then if ignored_syscalls[val] then
error("Ignored syscall \""..val.."\" in "..filter_type..": "..source) error("Ignored syscall \"" .. val .. "\" in " .. filter_type .. ": " .. source)
end end
end end
function check_event(val) function check_event(val)
if ignored_events[val] then if ignored_events[val] then
error("Ignored event \""..val.."\" in "..filter_type..": "..source) error("Ignored event \"" .. val .. "\" in " .. filter_type .. ": " .. source)
end end
end end
function cb(node) function cb(node)
if node.left.type == "FieldName" and if node.left.type == "FieldName" and (node.left.value == "evt.type" or node.left.value == "syscall.type") then
(node.left.value == "evt.type" or
node.left.value == "syscall.type") then
if (node.operator == "in" or if (node.operator == "in" or node.operator == "intersects" or node.operator == "pmatch") then
node.operator == "intersects" or for i, v in ipairs(node.right.elements) do
node.operator == "pmatch") then if v.type == "BareString" then
for i, v in ipairs(node.right.elements) do if node.left.value == "evt.type" then
if v.type == "BareString" then check_event(v.value)
if node.left.value == "evt.type" then else
check_event(v.value) check_syscall(v.value)
else end
check_syscall(v.value) end
end end
end else
end if node.right.type == "BareString" then
else if node.left.value == "evt.type" then
if node.right.type == "BareString" then check_event(node.right.value)
if node.left.value == "evt.type" then else
check_event(node.right.value) check_syscall(node.right.value)
else end
check_syscall(node.right.value) end
end end
end end
end end
end
end
parser.traverse_ast(ast, {BinaryRelOp=1}, cb) parser.traverse_ast(ast, {
BinaryRelOp = 1
}, cb)
end end
-- Examine the ast and find the event types/syscalls for which the -- Examine the ast and find the event types/syscalls for which the
@@ -75,125 +72,129 @@ end
function sinsp_rule_utils.get_evttypes_syscalls(name, ast, source, warn_evttypes, verbose) function sinsp_rule_utils.get_evttypes_syscalls(name, ast, source, warn_evttypes, verbose)
local evttypes = {} local evttypes = {}
local syscallnums = {} local syscallnums = {}
local evtnames = {} local evtnames = {}
local found_event = false local found_event = false
local found_not = false local found_not = false
local found_event_after_not = false local found_event_after_not = false
function cb(node) function cb(node)
if node.type == "UnaryBoolOp" then if node.type == "UnaryBoolOp" then
if node.operator == "not" then if node.operator == "not" then
found_not = true found_not = true
end end
else else
if node.operator == "!=" then if node.operator == "!=" then
found_not = true found_not = true
end end
if node.left.type == "FieldName" and node.left.value == "evt.type" then if node.left.type == "FieldName" and node.left.value == "evt.type" then
found_event = true found_event = true
if found_not then if found_not then
found_event_after_not = true found_event_after_not = true
end end
if (node.operator == "in" or if (node.operator == "in" or node.operator == "intersects" or node.operator == "pmatch") then
node.operator == "intersects" or for i, v in ipairs(node.right.elements) do
node.operator == "pmatch") then if v.type == "BareString" then
for i, v in ipairs(node.right.elements) do
if v.type == "BareString" then
-- The event must be a known event -- The event must be a known event
if events[v.value] == nil and syscalls[v.value] == nil then if events[v.value] == nil and syscalls[v.value] == nil then
error("Unknown event/syscall \""..v.value.."\" in filter: "..source) error("Unknown event/syscall \"" .. v.value .. "\" in filter: " .. source)
end end
evtnames[v.value] = 1 evtnames[v.value] = 1
if events[v.value] ~= nil then if events[v.value] ~= nil then
for id in string.gmatch(events[v.value], "%S+") do for id in string.gmatch(events[v.value], "%S+") do
evttypes[id] = 1 evttypes[id] = 1
end end
end end
if syscalls[v.value] ~= nil then if syscalls[v.value] ~= nil then
for id in string.gmatch(syscalls[v.value], "%S+") do for id in string.gmatch(syscalls[v.value], "%S+") do
syscallnums[id] = 1 syscallnums[id] = 1
end end
end end
end end
end end
else else
if node.right.type == "BareString" then if node.right.type == "BareString" then
-- The event must be a known event -- The event must be a known event
if events[node.right.value] == nil and syscalls[node.right.value] == nil then if events[node.right.value] == nil and syscalls[node.right.value] == nil then
error("Unknown event/syscall \""..node.right.value.."\" in filter: "..source) error("Unknown event/syscall \"" .. node.right.value .. "\" in filter: " .. source)
end end
evtnames[node.right.value] = 1 evtnames[node.right.value] = 1
if events[node.right.value] ~= nil then if events[node.right.value] ~= nil then
for id in string.gmatch(events[node.right.value], "%S+") do for id in string.gmatch(events[node.right.value], "%S+") do
evttypes[id] = 1 evttypes[id] = 1
end end
end end
if syscalls[node.right.value] ~= nil then if syscalls[node.right.value] ~= nil then
for id in string.gmatch(syscalls[node.right.value], "%S+") do for id in string.gmatch(syscalls[node.right.value], "%S+") do
syscallnums[id] = 1 syscallnums[id] = 1
end end
end end
end end
end end
end end
end end
end end
parser.traverse_ast(ast.filter.value, {BinaryRelOp=1, UnaryBoolOp=1} , cb) parser.traverse_ast(ast.filter.value, {
BinaryRelOp = 1,
UnaryBoolOp = 1
}, cb)
if not found_event then if not found_event then
if warn_evttypes == true then if warn_evttypes == true then
io.stderr:write("Rule "..name..": warning (no-evttype):\n") io.stderr:write("Rule " .. name .. ": warning (no-evttype):\n")
io.stderr:write(source.."\n") io.stderr:write(source .. "\n")
io.stderr:write(" did not contain any evt.type restriction, meaning it will run for all event types.\n") io.stderr:write(
io.stderr:write(" This has a significant performance penalty. Consider adding an evt.type restriction if possible.\n") " did not contain any evt.type restriction, meaning it will run for all event types.\n")
end io.stderr:write(
evttypes = {} " This has a significant performance penalty. Consider adding an evt.type restriction if possible.\n")
syscallnums = {} end
evtnames = {} evttypes = {}
end syscallnums = {}
evtnames = {}
end
if found_event_after_not then if found_event_after_not then
if warn_evttypes == true then if warn_evttypes == true then
io.stderr:write("Rule "..name..": warning (trailing-evttype):\n") io.stderr:write("Rule " .. name .. ": warning (trailing-evttype):\n")
io.stderr:write(source.."\n") io.stderr:write(source .. "\n")
io.stderr:write(" does not have all evt.type restrictions at the beginning of the condition,\n") io.stderr:write(" does not have all evt.type restrictions at the beginning of the condition,\n")
io.stderr:write(" or uses a negative match (i.e. \"not\"/\"!=\") for some evt.type restriction.\n") io.stderr:write(" or uses a negative match (i.e. \"not\"/\"!=\") for some evt.type restriction.\n")
io.stderr:write(" This has a performance penalty, as the rule can not be limited to specific event types.\n") io.stderr:write(
io.stderr:write(" Consider moving all evt.type restrictions to the beginning of the rule and/or\n") " This has a performance penalty, as the rule can not be limited to specific event types.\n")
io.stderr:write(" replacing negative matches with positive matches if possible.\n") io.stderr:write(" Consider moving all evt.type restrictions to the beginning of the rule and/or\n")
end io.stderr:write(" replacing negative matches with positive matches if possible.\n")
evttypes = {} end
syscallnums = {} evttypes = {}
evtnames = {} syscallnums = {}
end evtnames = {}
end
evtnames_only = {} evtnames_only = {}
local num_evtnames = 0 local num_evtnames = 0
for name, dummy in pairs(evtnames) do for name, dummy in pairs(evtnames) do
table.insert(evtnames_only, name) table.insert(evtnames_only, name)
num_evtnames = num_evtnames + 1 num_evtnames = num_evtnames + 1
end end
if num_evtnames == 0 then if num_evtnames == 0 then
table.insert(evtnames_only, "all") table.insert(evtnames_only, "all")
end end
table.sort(evtnames_only) table.sort(evtnames_only)
if verbose then if verbose then
io.stderr:write("Event types/Syscalls for rule "..name..": "..table.concat(evtnames_only, ",").."\n") io.stderr:write("Event types/Syscalls for rule " .. name .. ": " .. table.concat(evtnames_only, ",") .. "\n")
end end
return evttypes, syscallnums return evttypes, syscallnums
end end
return sinsp_rule_utils return sinsp_rule_utils