Skip to content

Commit 01a3819

Browse files
William Chrispwilliamchrisp
authored andcommitted
Initial custom publisher files and test.
1 parent 911df51 commit 01a3819

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1454
-1
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ integration-test:
2222
pipenv run pytest -m 'integration' tests/
2323

2424
test:
25-
pipenv run pytest --cov=src --cov-fail-under=81 --cov-report term-missing tests/
25+
pipenv run pytest --cov=src --cov=publisher --cov-fail-under=81 --cov-report term-missing tests/
2626

2727
watch:
2828
ptw -- -m 'not integration' tests/

publisher/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<h1 align="center">Flight Controller Custom Publisher</h1>
2+
<p align="center">Allows you to publish custom events for flight controller to produce metrics.</p>
3+
4+
# Usage
5+
6+
## Prerequsites
7+
- Python version 3.10
8+
- Pipenv (version 2022.10.12 proven working)
9+
- Ensure you have working AWS account creds/tokens.
10+
11+
`make local` - Install python prerequisites and enter the pip environment.
12+
13+
## Base command:
14+
`python -m publisher.entrypoints.main`
15+
16+
## Arguments:
17+
18+
| Option (Short) | Options (Long) | Description |
19+
|:--------------:|:--------------:|:------------------------------------------------------------------------------:|
20+
| -h | --help | Shows command help and all available options |
21+
| -so | --source | {open_policy_agent,checkov} - The source in which you want to get events from. |
22+
| -f | --file | The json file in which to parse result into events. |
23+
| -si | --sink | {event_bridge} - The sink in which you want to send events to. |
24+
25+
## Example Workflow:
26+
27+
You can test with the examples in `tests/examples` and just running the the publisher tool or by using in your workflow like below.
28+
29+
**Custom Checkov Guardrail Events**
30+
1. `terraform plan --out tfplan.binary; terraform show -json tfplan.binary > tfplan.json`
31+
2. `checkov -f tfplan.json -o json checkov.json`
32+
3. `python -m publisher.entrypoints.main -so checkov -f tests/examples/checkov.json -si event_bridge`
33+
34+
**Custom Open Policy Agent Events**
35+
`python -m publisher.entrypoints.main -so open_policy_agent -f tests/examples/opa.json -si event_bridge`
36+
37+
**Example Workflow**
38+
1. Run Checkov on your Infrastructure as Code and output this to a .json file
39+
2. Run Flight Controller handler.py on this file to parse results and generate events.
40+
41+
**This can be done with in your CICD**
42+
43+
# Developing
44+
45+
## Code Structure
46+
The code is structured in the [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) pattern.
47+
48+
![Clean Architecture](../images/CleanArchitecture.jpeg)
49+
50+
The current layers are:
51+
52+
1. `Entities`, which contains domain objects
53+
2. `Drivers`, which interact with data storage
54+
3. `Entrypoints`, which handle the event from AWS, retrieve and store data through drivers and call adapters to perform the needed business logic
55+
56+
The core rule of Clean Architecture, is that a layer can only depend on the layers that have come before it. E.g. code in the `usecases` layer, may depend on `entities`, but cannot depend on `adapters` or `drivers`.
57+
58+
When developing, it is simplest to start at the first layer and work down ending up with the entrypoint. This forces you to focus on the domain objects first before considering external services.
59+
60+
## Adding more support
61+
62+
Adding support for a new source:
63+
`publisher/drivers/`
64+
65+
Adding support for a new event type:
66+
`publisher/entities/`
67+
68+
Adding support for a new sink:
69+
`publisher/drivers/`
File renamed without changes.
File renamed without changes.

publisher/drivers/checkov.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from time import time
2+
from typing import Tuple, Union
3+
from uuid import uuid4
4+
5+
import structlog
6+
7+
from publisher.drivers.event_source import EventSource
8+
from publisher.drivers.file_source import FileSource
9+
from publisher.entities.events import Event
10+
from publisher.entities.guardrail import (
11+
GuardrailActivated,
12+
GuardrailActivatedDetail,
13+
GuardrailPassed,
14+
GuardrailPassedDetail,
15+
)
16+
17+
LOGGER = structlog.get_logger(__name__)
18+
19+
20+
class Checkov(EventSource):
21+
def __init__(self) -> None:
22+
self.file_source = FileSource()
23+
24+
def get_events(self, file: str) -> Union[Exception, Tuple[Event]]:
25+
current_time = int(time())
26+
data = self.file_source.read_file(file)
27+
if isinstance(data, Exception):
28+
LOGGER.error(f"Unable to read Checkov results file: {file}", exception=str(data))
29+
return data
30+
events = []
31+
if "results" in data:
32+
for result in data["results"]["passed_checks"]:
33+
events.append(GuardrailPassed(
34+
source = "contino.custom",
35+
detail_type = "Checkov Guardrail Passed",
36+
detail = GuardrailPassedDetail(
37+
aggregate_id = result["resource"], # Will need a better way of getting resource id, current method is not live id
38+
guardrail_id = result["check_id"],
39+
time = current_time,
40+
)
41+
))
42+
for result in data["results"]["failed_checks"]:
43+
events.append(GuardrailActivated(
44+
source = "contino.custom",
45+
detail_type = "Checkov Guardrail Activated",
46+
detail = GuardrailActivatedDetail(
47+
aggregate_id = result["resource"], # Will need a better way of getting resource id, current method is not live id
48+
guardrail_id = result["check_id"],
49+
time = current_time,
50+
)
51+
))
52+
return tuple(events)
53+
LOGGER.error(f"Unable to read Checkov results from file: {file}")
54+
return Exception(f"Unable to read Checkov results from file: {file}")
55+

publisher/drivers/event_bridge.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import json
2+
from typing import Dict, Any, Optional, List, Union
3+
4+
import boto3
5+
import structlog
6+
7+
from publisher.drivers.event_sink import EventSink
8+
from publisher.entities.events import Event
9+
10+
11+
LOGGER = structlog.get_logger(__name__)
12+
13+
14+
class EventBridge(EventSink):
15+
def __init__(self) -> None:
16+
self.event_bridge_client = boto3.client("events")
17+
18+
def _split_events(self, events: List[Dict[str, Any]]) -> Any:
19+
# Will split events based on eventbridge constraint for 10 at a time
20+
for i in range(0, len(events), 10):
21+
yield events[i:i + 10]
22+
23+
def _format_events(self, event: Dict) -> Dict:
24+
return {
25+
"Source": event.source,
26+
"DetailType": event.detail_type,
27+
"Detail": json.dumps(event.detail.__dict__),
28+
"EventBusName": event.event_bus_name,
29+
}
30+
31+
def send_events(self, events: List[Event]) -> Optional[Union[Exception, str]]:
32+
try:
33+
events = [self._format_events(event) for event in events]
34+
if len(events) > 10:
35+
event_groups = self._split_events(events)
36+
for event_group in event_groups:
37+
response = self.event_bridge_client.put_events(Entries=event_group)
38+
else:
39+
response = self.event_bridge_client.put_events(Entries=events)
40+
return str(response)
41+
except Exception as err:
42+
return err
43+

publisher/drivers/event_sink.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from abc import ABC, abstractmethod
2+
from typing import List, Optional, Union
3+
4+
from publisher.entities.events import Event
5+
6+
7+
class EventSink(ABC):
8+
@abstractmethod
9+
def send_events(self, events: List[Event]) -> Optional[Union[Exception, str]]:
10+
raise NotImplementedError() # pragma: no cover

publisher/drivers/event_source.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Tuple, Union
3+
4+
from publisher.entities.events import Event
5+
6+
7+
class EventSource(ABC):
8+
@abstractmethod
9+
def get_events(self, file: str) -> Union[Exception, Tuple[Event]]:
10+
raise NotImplementedError() # pragma: no cover
11+

publisher/drivers/file_source.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import json
2+
from typing import Dict
3+
4+
import structlog
5+
6+
LOGGER = structlog.get_logger(__name__)
7+
8+
9+
class FileSource():
10+
def read_file(self, file: str) -> Dict:
11+
try:
12+
with open(file, "r") as file:
13+
return json.loads(file.read())
14+
except FileNotFoundError as err:
15+
LOGGER.error(f"File {file} not found!", exception=str(err))
16+
return err
17+
except Exception as err:
18+
LOGGER.error(f"file_source.py raised an exception", exception=str(err))
19+
return err
20+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from time import time
2+
from typing import Tuple, Union
3+
4+
import structlog
5+
6+
from publisher.drivers.event_source import EventSource
7+
from publisher.drivers.file_source import FileSource
8+
from publisher.entities.events import Event
9+
from publisher.entities.guardrail import (
10+
GuardrailActivated,
11+
GuardrailActivatedDetail,
12+
GuardrailPassed,
13+
GuardrailPassedDetail,
14+
)
15+
16+
LOGGER = structlog.get_logger(__name__)
17+
18+
class OpenPolicyAgent(EventSource):
19+
def __init__(self) -> None:
20+
self.file_source = FileSource()
21+
22+
def get_events(self, file: str) -> Union[Exception, Tuple[Event]]:
23+
current_time = int(time())
24+
data = self.file_source.read_file(file)
25+
if isinstance(data, Exception):
26+
LOGGER.error(f"Unable to read Open Policy Agent results file: {file}", exception=str(data))
27+
return data
28+
events = []
29+
if "results" in data:
30+
for result in data["results"]:
31+
if result["allow"] == True:
32+
events.append(GuardrailPassed(
33+
source = "contino.custom",
34+
detail_type = "Open Policy Agent Guardrail Passed",
35+
detail = GuardrailPassedDetail(
36+
aggregate_id = result["input"]["metadata"]["name"] + "-" + result["input"]["metadata"]["namespace"],
37+
guardrail_id = result["query"],
38+
time = current_time,
39+
)
40+
))
41+
else:
42+
events.append(GuardrailActivated(
43+
source = "contino.custom",
44+
detail_type = "Open Policy Agent Guardrail Activated",
45+
detail = GuardrailActivatedDetail(
46+
aggregate_id = result["input"]["metadata"]["name"] + "-" + result["input"]["metadata"]["namespace"],
47+
guardrail_id = result["query"],
48+
time = current_time,
49+
)
50+
))
51+
return tuple(events)
52+
LOGGER.error(f"Unable to read Open Policy Agent results from file: {file}")
53+
return Exception(f"Unable to read Open Policy Agent results from file: {file}")
54+

0 commit comments

Comments
 (0)