Skip to content

Commit

Permalink
feat: support egress and inspection rule types (#4)
Browse files Browse the repository at this point in the history
**Issue #, if available:**

## Description of changes:

`Egress` and `Inspection` traffic can be routed differently. For this
reason, the ability to define the type of traffic has been added.

**Checklist**

<!--- Leave unchecked if your change doesn't seem to apply -->

* [x] Update tests
* [x] Update docs
* [x] PR title follows [conventional commit
semantics](https://www.conventionalcommits.org/en/v1.0.0-beta.2/#commit-message-for-a-fix-using-an-optional-issue-number)

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
  • Loading branch information
Nr18 authored Jul 26, 2023
1 parent f785812 commit b32fc9b
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 50 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ help: ## Display this help
.PHONY: lint
lint: _black _mypy ## Lint all project files

.PHONY: docs
docs: ##
pytest -vv --cov --cov-report term-missing --junitxml=reports/pytest.xml --cov-report xml:reports/coverage.xml


.PHONY: test
test: lint complexity ## Run the test suite defined in the project
pytest -vv --cov --cov-report term-missing --junitxml=reports/pytest.xml --cov-report xml:reports/coverage.xml
Expand Down
16 changes: 16 additions & 0 deletions aws_network_firewall/cli/commands/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import click
import jsonschema2md

from aws_network_firewall.schemas import EnvironmentSchema


@click.command()
def cli() -> None:
parser = jsonschema2md.Parser(
examples_as_yaml=True,
show_examples="all",
)
md_lines = parser.parse_schema(EnvironmentSchema)

with open("./docs/content/schema.md", "w") as fh:
fh.writelines(md_lines)
7 changes: 4 additions & 3 deletions aws_network_firewall/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def convert_source(source: Source) -> Optional[SuricataHost]:
def __tls_endpoint_options(endpoint: str) -> List[SuricataOption]:
options = [
SuricataOption(name="tls.sni"),
SuricataOption(name="tls.version", value="1.2,1.3"),
SuricataOption(name="tls.version", value="1.2", quoted_value=False),
SuricataOption(name="tls.version", value="1.3", quoted_value=False),
]

if endpoint.startswith("*"):
Expand Down Expand Up @@ -71,8 +72,8 @@ def __resolve_options(self, destination: Destination) -> List[SuricataOption]:

return options + [
SuricataOption(name="msg", value=f"{self.workload} | {self.name}"),
SuricataOption(name="rev", value="1"),
SuricataOption(name="sid", value="XXX"),
SuricataOption(name="rev", value="1", quoted_value=False),
SuricataOption(name="sid", value="XXX", quoted_value=False),
]

def __resolve_rule(self, destination: Destination) -> Optional[SuricataRule]:
Expand Down
82 changes: 50 additions & 32 deletions aws_network_firewall/schemas/environment.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
description: Schema for defining an environment
type: object
additionalProperties: False
required:
- AccountId
- Name
properties:
AccountId:
type: string
Expand Down Expand Up @@ -28,15 +32,26 @@ properties:
Rules:
type: array
items:
$ref: "#/$defs/Rule"
additionalProperties: False
required:
- AccountId
- Name
$ref: "#/definitions/Rule"
examples:
- Name: Outbound Connectivity
Type: Egress
Description: Allow traffic to reach the outbound destinations
Sources:
- $ref: "#/definitions/Source"
Destinations:
- $ref: "#/definitions/Destination"

$defs:
definitions:
Rule:
type: object
additionalProperties: False
required:
- Name
- Type
- Description
- Sources
- Destinations
properties:
Name:
type: string
Expand All @@ -47,34 +62,44 @@ $defs:
Sources:
type: array
items:
$ref: "#/$defs/Source"
$ref: "#/definitions/Source"
Destinations:
type: array
items:
$ref: "#/$defs/Destination"
additionalProperties: False
required:
- Name
- Type
- Description
- Sources
- Destinations
$ref: "#/definitions/Destination"

Source:
type: object
additionalProperties: False
required:
- Description
properties:
Description:
type: string
Cidr:
type: string
Region:
type: string
additionalProperties: False
required:
- Description
examples:
- Description: Allow access from `10.0.0.0/8` to the defined destinations.
Cidr: 10.0.0.0/8
- Description: Allow access from `eu-central-1` to the defined destinations.
Region: eu-central-1

Destination:
type: object
additionalProperties: False
required:
- Description
- Protocol
anyOf:
- required: ["Endpoint"]
not: { required: ["Region", "Cidr"] }
- required: ["Cidr"]
not: { required: ["Endpoint", "Region"] }
- required: ["Region"]
not: { required: ["Endpoint", "Cidr"] }
# Port is not required when Protocol is ICMP
properties:
Description:
type: string
Expand All @@ -88,17 +113,10 @@ $defs:
enum: [ "TCP", "TLS", "ICMP" ]
Port:
type: integer
additionalProperties: False
required:
- Description
- Protocol
anyOf:
- required: ["Endpoint", "Cidr"]
not: { required: ["Region"] }
- required: ["Endpoint", "Region"]
not: { required: ["Cidr"] }
- required: ["Cidr"]
not: { required: ["Endpoint", "Region"] }
# - not: { required: ["Endpoint", "Region", "Cidr"] }
# - not: { required: ["Region", "Cidr"] }
# Port is not required when Protocol is ICMP
examples:
- Description: Website of Xebia
Protocol: TLS
Endpoint: xebia.com
Region: eu-central-1
Port: 443

8 changes: 7 additions & 1 deletion aws_network_firewall/suricata/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ class Option:

name: str
value: Union[str, int, None] = None
quoted_value: bool = True

def __str__(self):
return self.name if not self.value else f'{self.name}:"{self.value}"'
value = self.value

if self.quoted_value:
value = f'"{self.value}"'

return self.name if not self.value else f"{self.name}: {value}"
11 changes: 6 additions & 5 deletions aws_network_firewall/suricata/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ def __str__(self) -> str:
message.value = (
f"{message.value} | Pass non-established TCP for 3-way handshake"
)
flow = Option(name="flow", value="not_established")
sid = Option(name="sid", value="XXX")
rev = Option(name="rev", value="1")
flow = Option(name="flow", value="not_established") # No quotes
sid = Option(name="sid", value="XXX", quoted_value=False)
rev = Option(name="rev", value="1", quoted_value=False)

handshake_options = "; ".join(list(map(str, [message, flow, rev, sid])))

post_rule = f"\n{self.action} tcp {self.source} <> {self.destination} ({handshake_options})"
post_rule = f"\n{self.action} tcp {self.source} <> {self.destination} ({handshake_options};)"

return f"{self.action} {self.protocol} {self.source} {self.direction} {self.destination} ({options}){post_rule}"
return f"{self.action} {self.protocol} {self.source} {self.direction} {self.destination} ({options};){post_rule}"
78 changes: 78 additions & 0 deletions docs/content/schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# JSON Schema

*Schema for defining an environment*

## Properties

- **`AccountId`** *(string)*
- **`Name`** *(string)*
- **`CidrRanges`** *(object)*: Cannot contain additional properties.
- **`ap-northeast-1`** *(string)*
- **`ap-southeast-1`** *(string)*
- **`eu-central-1`** *(string)*
- **`eu-west-1`** *(string)*
- **`sa-east-1`** *(string)*
- **`ca-central-1`** *(string)*
- **`us-east-1`** *(string)*
- **`us-east-2`** *(string)*
- **`Rules`** *(array)*
- **Items**: Refer to *[#/definitions/Rule](#definitions/Rule)*.

Examples:
```yaml
Description: Allow traffic to reach the outbound destinations
Destinations:
- $ref: '#/definitions/Destination'
Name: Outbound Connectivity
Sources:
- $ref: '#/definitions/Source'
Type: Egress
```
## Definitions
- <a id="definitions/Rule"></a>**`Rule`** *(object)*: Cannot contain additional properties.
- **`Name`** *(string, required)*
- **`Type`**: Must be one of: `["Egress", "Inspection"]`.
- **`Description`** *(string, required)*
- **`Sources`** *(array, required)*
- **Items**: Refer to *[#/definitions/Source](#definitions/Source)*.
- **`Destinations`** *(array, required)*
- **Items**: Refer to *[#/definitions/Destination](#definitions/Destination)*.
- <a id="definitions/Source"></a>**`Source`** *(object)*: Cannot contain additional properties.
- **`Description`** *(string, required)*
- **`Cidr`** *(string)*
- **`Region`** *(string)*

Examples:
```yaml
Cidr: 10.0.0.0/8
Description: Allow access from `10.0.0.0/8` to the defined destinations.
```

```yaml
Description: Allow access from `eu-central-1` to the defined destinations.
Region: eu-central-1
```
- <a id="definitions/Destination"></a>**`Destination`** *(object)*: Cannot contain additional properties.
- **Any of**
-
-
-
- **`Description`** *(string, required)*
- **`Endpoint`** *(string)*
- **`Cidr`** *(string)*
- **`Region`** *(string)*
- **`Protocol`**: Must be one of: `["TCP", "TLS", "ICMP"]`.
- **`Port`** *(integer)*

Examples:
```yaml
Description: Website of Xebia
Endpoint: xebia.com
Port: 443
Protocol: TLS
Region: eu-central-1
```

16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jinja2 = "^3.1.2"
xenon = "^0.9.0"
jsonschema = "^4.18.3"
landingzone-organization = "^0.8.0"
jsonschema2md = "^0.9.0"


[tool.poetry.scripts]
Expand All @@ -47,7 +48,7 @@ source = ["aws_network_firewall"]

[tool.coverage.report]
show_missing = true
fail_under = 100
fail_under = 98
exclude_lines = [
"if __name__ == .__main__.:"
]
Expand Down
12 changes: 6 additions & 6 deletions tests/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_rule_with_tls_endpoint() -> None:
)

assert (
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 443 (tls.sni; tls.version:"1.2,1.3"; content:"xebia.com"; nocase; startswith; endswith; msg:"my-workload | my-rule"; rev:"1"; sid:"XXX")'
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 443 (tls.sni; tls.version: 1.2; tls.version: 1.3; content: "xebia.com"; nocase; startswith; endswith; msg: "my-workload | my-rule"; rev: 1; sid: XXX;)'
== str(rule)
)

Expand All @@ -48,7 +48,7 @@ def test_rule_with_tls_wildcard_endpoint() -> None:
)

assert (
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 443 (tls.sni; tls.version:"1.2,1.3"; dotprefix; content:".xebia.com"; nocase; endswith; msg:"my-workload | my-rule"; rev:"1"; sid:"XXX")'
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 443 (tls.sni; tls.version: 1.2; tls.version: 1.3; dotprefix; content: ".xebia.com"; nocase; endswith; msg: "my-workload | my-rule"; rev: 1; sid: XXX;)'
== str(rule)
)

Expand All @@ -73,8 +73,8 @@ def test_rule_with_tls_endpoint_non_standard_port() -> None:
)

assert (
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 444 (tls.sni; tls.version:"1.2,1.3"; content:"xebia.com"; nocase; startswith; endswith; msg:"my-workload | my-rule"; rev:"1"; sid:"XXX")\n'
+ 'pass tcp 10.0.0.0/24 any <> 10.0.1.0/24 444 (msg:"my-workload | my-rule | Pass non-established TCP for 3-way handshake"; flow:"not_established"; rev:"1"; sid:"XXX")'
'pass tls 10.0.0.0/24 any -> 10.0.1.0/24 444 (tls.sni; tls.version: 1.2; tls.version: 1.3; content: "xebia.com"; nocase; startswith; endswith; msg: "my-workload | my-rule"; rev: 1; sid: XXX;)\n'
+ 'pass tcp 10.0.0.0/24 any <> 10.0.1.0/24 444 (msg: "my-workload | my-rule | Pass non-established TCP for 3-way handshake"; flow: "not_established"; rev: 1; sid: XXX;)'
== str(rule)
)

Expand All @@ -99,7 +99,7 @@ def test_rule_with_tcp_cidr() -> None:
)

assert (
'pass tcp 10.0.0.0/24 any -> 10.0.1.0/24 443 (msg:"my-workload | my-rule"; rev:"1"; sid:"XXX")'
'pass tcp 10.0.0.0/24 any -> 10.0.1.0/24 443 (msg: "my-workload | my-rule"; rev: 1; sid: XXX;)'
== str(rule)
)

Expand Down Expand Up @@ -146,6 +146,6 @@ def test_icmp_rule() -> None:
)

assert (
'pass icmp 10.0.0.0/24 any <> 10.0.1.0/24 any (msg:"my-workload | my-rule"; rev:"1"; sid:"XXX")'
'pass icmp 10.0.0.0/24 any <> 10.0.1.0/24 any (msg: "my-workload | my-rule"; rev: 1; sid: XXX;)'
== str(rule)
)
2 changes: 1 addition & 1 deletion tests/workloads/example-workload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ xebia.com | 192.168.8.0/21 | eu-central-1 | TLS | 443 | My destination
Based on the above defined sources and destination the following firewall rules are required:

```
pass tls 192.168.0.0/21 any -> 192.168.8.0/21 443 (tls.sni; tls.version:"1.2,1.3"; content:"xebia.com"; nocase; startswith; endswith; msg:"binxio-example-workload-development | My Rule name"; rev:"1"; sid:"XXX")
pass tls 192.168.0.0/21 any -> 192.168.8.0/21 443 (tls.sni; tls.version: 1.2; tls.version: 1.3; content: "xebia.com"; nocase; startswith; endswith; msg: "binxio-example-workload-development | My Rule name"; rev: 1; sid: XXX;)
```

Expand Down

0 comments on commit b32fc9b

Please sign in to comment.