Falco 的检测引擎从内核侧采集系统调用事件,然后逐条与规则文件中的条件进行匹配。规则文件是纯文本 YAML,加载后会被解析成一组规则对象,每个对象都包含触发条件、输出文本和优先级。理解这一点很重要:Falco 本身并不内置某一条具体的安全策略,它只提供事件流和字段访问能力,真正决定什么行为算异常的是你所编写的规则。因此一个条件写得宽泛的规则,可能每分钟产生几百条告警;而字段选择不当的规则,又可能因为事件属性不匹配而永远不触发。

编写 Falco 规则时,要关注的不是简单地把几个关键词堆在一起,而是先想清楚要检测的行为发生在哪些系统调用事件上,再选择这些事件能够提供的字段进行过滤。以容器中启动 shell 为例,它对应的是 execve 和 execveat 系统调用,而不是 open 或 connect。只有把事件类型、字段作用域和条件表达式三者的关系理顺,规则才能在正确的时间命中正确的事件。
一、规则文件的核心结构
一个 Falco 规则文件本质上是一个 YAML 列表,列表中的每一项可以是 rule、macro 或 list。rule 是最终产生告警的单元,macro 用来复用条件片段,list 用来定义可枚举的值集合。三个组件配合使用,可以显著减少重复条件,避免规则文件越写越不可维护。
规则对象中的 condition 字段使用 Falco 的条件表达式语法,而非任意 Python 或 Shell 表达式。这个表达式会被规则引擎编译,对每一个进入的事件求值。描述字段 desc 用于说明这条规则要检测什么,不做匹配用途;output 是触发后的告警文本;priority 决定告警级别;tags 是标签数组,常用于前端过滤和路由。
- rule: Container Bash Spawned desc: Detect bash or sh spawned inside a container condition: spawned_process and in_container and proc.name in (bash, sh) output: 'Shell spawned in container (user=%user.name container_id=%container.id proc=%proc.name)' priority: WARNING tags: [container, process, shell]
上面这条规则引用了 spawned_process 和 in_container 两个宏,并没有直接在 condition 中展开所有条件。初次接触时,建议先把宏替换成完整条件来理解:spawned_process 可以定义为 evt.type in (execve, execveat),in_container 可以定义为 container.id != host。这样规则才具备完整语义。实际生产环境中,很多官方规则正是用宏来抽象常见事件序列和上下文判断。
二、条件表达式:事件类型与字段过滤
Falco 条件表达式的核心语法与其他过滤型 DSL 类似,支持逻辑运算、比较运算、列表包含和字符串匹配。最常用的逻辑是 and、or、not,列表包含写作 in,例如 evt.type in (open, openat, openat2)。如果想排除某些值,可以使用 not proc.name in (curl, wget)。比较运算支持等于、不等于、大于、小于,但实际规则里更常见的是直接判断字段是否存在于某个列表。
写条件时最关键的一点是明确事件类型。Falco 的字段按事件来源和作用域划分,不是所有字段在所有事件上都存在。比如 fd.name 在文件打开、关闭事件里有值,在 execve 事件里通常就是空的;proc.cmdline 在执行类事件中才有意义。很多新规则不触发,往往是因为把文件路径字段用在了进程创建事件上,或者漏掉了对 evt.type 的限定,导致引擎把大量不相关事件都纳入匹配范围。
建议从系统调用事件类型入手,而不是从进程名直接写规则。先确定要监控的行为发生在哪几类事件上,再选择这些事件共有的字段做过滤。例如检测容器内启动 shell,相应的系统调用是 execve 和 execveat,因此条件至少要包含 evt.type in (execve, execveat)。之后再加上进程名、容器上下文等条件,告警才会比较稳定。
- rule: Shell from Netcat Connection desc: Detect shell spawned by netcat condition: 'spawned_process and proc.pname in (nc, netcat, ncat) and proc.name in (bash, sh, zsh)' output: 'Netcat shell detected (user=%user.name pid=%proc.pid cmdline=%proc.cmdline)' priority: CRITICAL tags: [network, shell]
这段条件里第一个宏 spawned_process 已经把事件类型锁定为 execve 和 execveat,接下来再判断父进程名和当前进程名。这里的 proc.pname 表示父进程名,proc.name 表示当前进程名。如果去掉 spawned_process,规则就会在大量非进程创建事件上做无效判断,虽然不一定会误报,但会明显增加匹配开销,并且可能因为字段和事件类型不匹配而出现诡异结果。
三、宏与列表:把规则拆成可复用片段
当规则文件超过几十条后,不同规则之间经常出现重复条件,例如判断容器环境、判断进程是否属于包管理器、判断文件是否写入二进制目录。如果每次都复制粘贴,后续维护会非常痛苦。Falco 的 macro 和 list 就是为解决复用问题设计的。macro 可以把一段条件表达式命名保存,list 则把一组字符串或数字集中管理。
macro 的定义本身不产生告警,只有被 rule 的 condition 引用时才会参与匹配。list 可以在条件里直接作为 in 操作符的右侧集合使用,例如 proc.name in (shell_binaries),其中 shell_binaries 是一个已定义的列表。这种写法比罗列一长串进程名清晰,也比把所有进程名写死在每条规则里更容易更新。
- list: shell_binaries items: [bash, sh, zsh, fish, dash] - macro: spawned_process condition: evt.type in (execve, execveat) - macro: container condition: container.id != host - rule: Shell in Container desc: Detect shell spawned inside container condition: spawned_process and container and proc.name in (shell_binaries) output: 'Container shell (user=%user.name container_id=%container.id shell=%proc.name)' priority: WARNING tags: [container, shell]
宏和列表虽然能简化规则,但也要注意命名清晰。宏名称最好表达一种状态或动作,像 spawned_process、container、write_binary_dir 就是可读性较好的命名;如果起成 cond1、macro2 这类名字,后期排查规则时反而会降低效率。另外,宏内部仍然要注意事件类型限定,否则被多条规则引用后,一条含糊的宏会把问题扩散到整个规则集。
在维护层面,宏和列表通常放在规则文件的顶部,或者单独拆到一个公共文件里。Falco 启动时顺序加载规则文件,宏和列表不需要在引用之前定义,但同一个文件中保持自上而下的排列更符合阅读习惯。对于需要临时覆盖某条官方规则的场景,可以复制规则到本地文件,修改后通过本地规则文件加载,官方文档也提供了规则覆盖机制来减少直接改官方文件带来的升级问题。
四、输出格式化与告警运营
规则被触发后,output 字段的内容会作为告警文本发往配置的输出通道。输出文本支持字段占位符,格式为百分号加字段名,例如 %proc.name、%container.id、%fd.name。在写 output 时,应该尽量包含定位问题所需的关键信息,比如用户、进程、容器、文件路径、命令行,而不是只写一句 Shell detected。
优先级字段 priority 实际上不改变检测逻辑,但会影响告警的展示和后续处理。Falco 支持的优先级从高到低依次为 emergency、alert、critical、error、warning、notice、informational、debug。运维人员可以根据优先级配置不同通知渠道,例如 critical 级别走即时通讯,warning 级别只写入日志。合理设置优先级,也能帮助接收方快速区分需要立即处理的事件和需要观察的事件。
tags 是另一个容易被低估的字段。标签本身不参与条件匹配,但可以在 Falcosidekick 等告警聚合组件中用来路由、分组、过滤。常见的标签包括 container、process、network、filesystem、mitre_technique 等。保持标签体系一致,有助于后续做告警统计和策略优化。不要随意写临时标签,否则时间一长,标签维度会变得零散,无法形成有效的运营闭环。
对于误报较多的规则,可以优先通过调整条件来降低误报,而不是立刻删除规则。例如某些合法进程也会调用 shell,可以加入父进程白名单,或者使用 exceptions 语法针对特定字段组合排除。Falco 的 exceptions 允许为一条规则配置多个例外条件,当事件命中例外组合时,该规则不再告警。这样比在 condition 里堆叠大量 not 条件更容易理解和维护。
五、调试方法:从规则不触发到精准告警
写完规则后最常见的问题是规则不触发,或者触发量远超预期。排查时第一步是确认规则文件是否真的被 Falco 加载。可以启动时指定规则文件,观察日志中是否有加载报错。如果 YAML 缩进错误、字段拼写错误,Falco 通常会在启动阶段直接报错;但如果是条件里引用了不存在的字段,可能不会报错,只是永远匹配不到事件。
为了验证规则条件,可以把 condition 临时改宽,先确认规则本身能被加载并产生输出。例如只保留 evt.type in (execve, execveat),看能否在触发目标行为时收到告警;然后再逐步加回进程名、容器 ID、路径等过滤条件。每加一层条件就重新测试一次,能比较快地定位是哪一层过滤把预期事件挡掉了。
测试时可以在容器或主机上构造对应行为。比如测试 shell 监测规则,就执行 docker exec 进入容器运行 sh;测试文件写入规则,就在监控目录下创建一个文件。观察输出不要只看控制台,还要检查 Falco 配置的输出目的地,可能是标准输出、文件、Syslog 或 HTTP。某些环境下控制台只有启动日志,告警事件被写到别处,容易误认为规则没生效。
另一个实用技巧是使用临时告警输出包含关键字段,例如 evt.type=%evt.type proc=%proc.name raw=%evt.raw,这在复杂条件中很有帮助。%evt.raw 可以输出原始事件数据,但内容较多,不适合长期保留,只建议在排查阶段使用。定位到问题后,再收紧输出,避免日志膨胀和敏感信息泄露。