blob: cd5d0ef28caa830843ce417f67a1e53d7fe96f02 [file]
import re
from enum import Enum
import ipaddress
class RuleAction(Enum):
ALLOW = 1
DENY = 0
class RuleSet:
def __init__(self, rules):
self.rules = rules
async def is_fact_allowed(self, fact):
allowed = True
for rule in await self._applicable_rules(fact):
if await self._is_ip_rule_match(rule, fact):
allowed = await self._rule_judgement(rule.action)
continue
if await self._is_regex_rule_match(rule, fact):
allowed = await self._rule_judgement(rule.action)
return allowed
async def _applicable_rules(self, fact):
applicable_rules = []
for rule in self.rules:
if rule.trait == fact.trait:
applicable_rules.append(rule)
return applicable_rules
async def apply_rules(self, facts):
if await self._has_rules():
valid_facts = []
for fact in facts:
if await self.is_fact_allowed(fact):
valid_facts.append(fact)
return [valid_facts]
else:
return [facts]
async def _has_rules(self):
return len(self.rules)
@staticmethod
async def _rule_judgement(action):
if action.value == RuleAction.DENY.value:
return False
return True
@staticmethod
async def _is_ip_network(value):
try:
ipaddress.IPv4Network(value)
return True
except (ValueError, ipaddress.AddressValueError):
pass
return False
@staticmethod
async def _is_ip_address(value):
try:
ipaddress.IPv4Address(value)
return True
except (ValueError, ipaddress.AddressValueError):
pass
return False
@staticmethod
async def _is_regex_rule_match(rule, fact):
return re.match(rule.match, fact.value)
async def _is_ip_rule_match(self, rule, fact):
""" We only match string-equivalent IP addresses, string-equivalent subnets in CIDR notation, and IP address
facts to the subnet rules where the address is a member of the subnet. Matching non-equivalent subnets is
complicated, because of the following general case:
_____________________________________
| Rule | Fact |
-------------------------------------
| DENY: 127.0.0.0/24 | 127.0.0.0/23 |
-------------------------------------
In the above case, we do not match on this fact, since the fact is a supernet of the rule (it "contains" the
rule subnet). Therefore, the rule subnet is only a portion of the fact subnet. Thus, Caldera would ignore the
DENY rule and scan /23 anyway. But this would include a denied subnet range, which is undesired behavior.
This being the case, Caldera does not match on non-equivalent subnets.
"""
if rule.match != '.*':
is_fact_address = await self._is_ip_address(fact.value)
is_fact_network = await self._is_ip_network(fact.value)
is_rule_address = await self._is_ip_address(rule.match)
is_rule_network = await self._is_ip_network(rule.match)
if is_fact_address and is_rule_address:
return fact.value == rule.match
elif is_fact_network and is_rule_address:
return False
elif is_fact_address and is_rule_network:
return ipaddress.IPv4Address(fact.value) in ipaddress.IPv4Network(rule.match)
elif is_fact_network and is_rule_network:
return fact.value == rule.match
return False