Rule definitions
You can define each custom rule in a single Python class file.
Each rule definition should have the following parts:
[required]
- rule_id
is a unique identifier among rules
- description
explains what the rule checks for.
- enabled
determines whether the rule is used or not.
[optional]
- tags
specifies one or more tags for including or excluding the rule.
- severity
represents the risk impact if the rule condition is matched.
- result_type
specifies the result type class instead of the default one.
match and check methods
Each rule definition should also have match
and check
methods:
match
takes the context
information and returns True if the rule should check this target.
When ARI is scanning a playbook, ARI updates the context by focusing on each runnable target such as Playbook, Role and Task.
The current object of the context can be accessed by ctx.current
, so if the rule is only for tasks, you can define a match
method like the following.
from dataclasses import dataclass
from ansible_risk_insight.models import AnsibleRunContext, RunTargetType, ExecutableType as ActionType
from ansible_risk_insight.rules.base import Rule
@dataclass
class NonBuiltinUseRule(Rule):
rule_id: str = "R110"
description: str = "Non-builtin module is used"
enabled: bool = True
def match(self, ctx: AnsibleRunContext) -> bool:
return ctx.current.type == RunTargetType.Task
check
also takes the context, but it returns a RuleResult object.
Normally, you can make this result object just by preparing the following 3 things.
result
represents if the target (e.g. task) met with the rule condition or notdetail
is the data shown in the result (console output / UI)- target itself (e.g. task or role)
Then you can call self.create_result()
like below to create result object.
In the example below, the rule condition is composed of 3 conditions - 1. the task invokes a module (not import/include), 2. the module was resolved and 3. the module is not "ansbile.builtin" one.
A task has a FQCN of the resolved module as task.resovled_name
, so the information is included in detail
.
def check(self, ctx: AnsibleRunContext):
task = ctx.current
result = (
task.action_type == ActionType.MODULE_TYPE and
task.resolved_action and
not task.resolved_action.startswith("ansible.builtin.")
)
detail = {
"fqcn": task.resolved_name,
}
rule_result = self.create_result(result=result, detail=detail, task=task)
return rule_result
Define custom rule result type
To customize the output format of the result data, you can define your own rule result class.
A custom rule result class should have a method print
:
print
is a method for formatting the output string of the rule result.
It can access rule properties by self._rule
, and it has detail as self.detail
.
Also it has the original file information of the task such as file name with self.file
and line number of the task block with self.lines
.
To enable your own RuleResult, you can specify the class name in the result_type
field in rules like below.
from dataclasses import dataclass
from ansible_risk_insight.rules.base import RuleResult
@dataclass
class NonBuiltinUseRuleResult(RuleResult):
def print(self):
output = f"ruleID={self._rule.rule_id}, \
description={self._rule.description}, \
result={self.result}, \
file={self.file}, \
lines={self.lines}, \
detail={self.detail}\n"
return output
@dataclass
class NonBuiltinUseRule(Rule):
rule_id: str = "R110"
description: str = "Non-builtin module is used"
enabled: bool = True
risk_type: type = NonBuiltinUseRuleResult