| 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 |