Skip to content

Commit

Permalink
Python client PoC
Browse files Browse the repository at this point in the history
Add grpc channel credentials

Use makefile instead of bash script

Fix time

build: makefile to download and build the protos

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

chore: do not commit .proto files

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

docs: example plus build instructions into the README

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

chore: remove protos and generated python code from the root

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

new: workaround the issues python has with code generated from protobuf

Read more here: protocolbuffers/protobuf#881

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

new(falco): generated python API into falco/schema and falco/svc

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

update(falco/domain): update the import paths

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>

update(falco): client import paths updated

Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>
  • Loading branch information
mmat authored and Mattia committed Feb 3, 2020
1 parent e8dcf8c commit e1a980e
Show file tree
Hide file tree
Showing 24 changed files with 958 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

.pytest_cache/

**/*.proto
39 changes: 39 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
SHELL := /bin/bash

PROTOC ?= $(shell which protoc)
GRPC_PYTHON_PLUGIN ?= $(shell which grpc_python_plugin)

PROTOS := protos/schema.proto protos/output.proto
PROTO_URLS := https://raw.githubusercontent.com/falcosecurity/falco/dev/userspace/falco/schema.proto https://raw.githubusercontent.com/falcosecurity/falco/dev/userspace/falco/output.proto
PROTO_SHAS := a1f427c114b945d0880b55058862b74015d036aa722985ca6e5474ab4ed19f69 4ce2f3e6d6ebc07a74535c4f21da73e44c6ef848ab83627b1ac987058be5ece9

PROTO_DIRS := $(dir ${PROTOS})
PROTO_DIRS_INCLUDES := $(patsubst %/, -I %, ${PROTO_DIRS})

SCHEMA_OUT_DIR := falco/schema
GRPC_OUT_DIR := falco/svc

.PHONY: protos
protos: ${PROTOS}

# $(1): the proto path
# $(2): the proto URL
# $(3): the proto SHA256
define download_rule
$(1):
@rm -f $(1)
@mkdir -p ${PROTO_DIRS} ${SCHEMA_OUT_DIR} ${GRPC_OUT_DIR}
@curl --silent -Lo $(1) $(2)
@echo $(3) $(1) | sha256sum -c
@sed -i '/option go_package/d' $(1)
${PROTOC} ${PROTO_DIRS_INCLUDES} --python_out=${SCHEMA_OUT_DIR} --grpc_out=${GRPC_OUT_DIR} --plugin=protoc-gen-grpc=${GRPC_PYTHON_PLUGIN} $(1)
endef
$(foreach PROTO,$(PROTOS),\
$(eval $(call download_rule,$(PROTO),$(firstword $(PROTO_URLS)),$(firstword $(PROTO_SHAS))))\
$(eval PROTO_URLS := $(wordlist 2,$(words $(PROTO_URLS)),$(PROTO_URLS)))\
$(eval PROTO_SHAS := $(wordlist 2,$(words $(PROTO_SHAS)),$(PROTO_SHAS)))\
)

.PHONY: clean
clean: ${PROTO_DIRS}
@rm -rf $^
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# client-py

> Python client and SDK for Falco
## Usage

### Output subscribe

```python
import falco
client = falco.Client(grpc_endpoint="localhost:5060", client_crt="/tmp/client.crt", client_key="/tmp/client.key", ca_root="/tmp/ca.crt")
for event in client.subscribe(falco.Request(keepalive=True)):
print(event)
```

## Development

### Dependencies

**TBD**

### Update protos

Perform the following edits to the Makefile:

1. Update the PROTOS array with the destination path of the .proto file.
2. Update the PROTO_URLS array with the URL from which to download it.
3. Update thr PROTO_SHAS array with the SHA256 sum of the file to download.
4. Execute the following commands:

```console
make clean
make protos
```
2 changes: 2 additions & 0 deletions falco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from falco.client import Client # noqa: F401
from falco.domain import Request, Response # noqa: F401
22 changes: 22 additions & 0 deletions falco/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import grpc

from falco.client_credentials import get_grpc_channel_credentials
from falco.domain import Response
from falco.svc.output_pb2_grpc import serviceStub


class Client:
def __init__(self, grpc_endpoint, client_crt, client_key, ca_root, *args, **kw):
self._client = serviceStub(
grpc.secure_channel(
grpc_endpoint,
credentials=get_grpc_channel_credentials(client_crt, client_key, ca_root),
options=[("grpc.max_receive_message_length", 1024 * 1024 * 512)],
),
)

def subscribe(self, request): # TODO: test
pb_req = request.to_proto()

for pb_resp in self._client.subscribe(pb_req):
yield Response.from_proto(pb_resp)
18 changes: 18 additions & 0 deletions falco/client_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import grpc

from falco.utils import load_file


def get_grpc_channel_credentials(client_crt, client_key, ca_root):
"""Returns a ChannelCredentials object to use with the grpc channel
https://grpc.github.io/grpc/python/grpc.html#create-client-credentials
"""

root_certificates = load_file(ca_root)
private_key = load_file(client_key)
certificate_chain = load_file(client_crt)

return grpc.ssl_channel_credentials(
root_certificates=root_certificates, private_key=private_key, certificate_chain=certificate_chain,
)
2 changes: 2 additions & 0 deletions falco/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from falco.domain.request import Request # noqa: F401
from falco.domain.response import Response # noqa: F401
18 changes: 18 additions & 0 deletions falco/domain/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from falco.schema.output_pb2 import request


class Request:
__slots__ = ("keepalive",)

def __init__(self, keepalive=None):
self.keepalive = keepalive

def __repr__(self):
return f"{self.__class__.__name__}(keepalive={self.keepalive})"

@classmethod
def from_proto(cls, pb_request):
return cls(keepalive=pb_request.keepalive)

def to_proto(self):
return request(keepalive=self.keepalive)
110 changes: 110 additions & 0 deletions falco/domain/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from datetime import datetime
from enum import Enum
from typing import Dict

from dateutil import tz

from falco.utils import pb_timestamp_from_datetime
from falco.schema.output_pb2 import response
from falco.schema.schema_pb2 import priority, source


class Response:
__slots__ = (
"time",
"_priority",
"_source",
"rule",
"output",
"output_fields",
"hostname",
)

class Priority(Enum):
EMERGENCY = "emergency"
ALERT = "alert"
CRITICAL = "critical"
ERROR = "error"
WARNING = "warning"
NOTICE = "notice"
INFORMATIONAL = "informational"
DEBUG = "debug"

PB_PRIORITY_TO_PRIORITY_MAP = {
0: Priority.EMERGENCY,
1: Priority.ALERT,
2: Priority.CRITICAL,
3: Priority.ERROR,
4: Priority.WARNING,
5: Priority.NOTICE,
6: Priority.INFORMATIONAL,
7: Priority.DEBUG,
}

class Source(Enum):
SYSCALL = "syscall"
K8S_AUDIT = "k8s_audit"

PB_SOURCE_TO_SOURCE_MAP = {
0: Source.SYSCALL,
1: Source.K8S_AUDIT,
}

def __init__(
self, time=None, priority=None, source=None, rule=None, output=None, output_fields=None, hostname=None,
):
self.time: datetime = time.astimezone(tz.tzutc())
self.priority: Response.Priority = priority
self.source: Response.Source = source
self.rule: str = rule
self.output: str = output
self.output_fields: Dict = output_fields
self.hostname: str = hostname

def __repr__(self):
return f"{self.__class__.__name__}(time={self.time}, priority={self.priority}, source={self.source}, rule={self.rule}, output={self.output}, output_fields={self.output_fields}, hostname={self.hostname})"

@property
def priority(self):
return self._priority

@priority.setter
def priority(self, p):
self._priority = None
if p and isinstance(p, Response.Priority):
self._priority = p

@property
def source(self):
return self._source

@source.setter
def source(self, s):
self._source = None
if s and isinstance(s, Response.Source):
self._source = s

@classmethod
def from_proto(cls, pb_response):
timestamp_dt = datetime.fromtimestamp(pb_response.time.seconds + pb_response.time.nanos / 1e9)

return cls(
time=timestamp_dt,
priority=Response.PB_PRIORITY_TO_PRIORITY_MAP[pb_response.priority],
source=Response.PB_SOURCE_TO_SOURCE_MAP[pb_response.source],
rule=pb_response.rule,
output=pb_response.output,
output_fields=pb_response.output_fields,
hostname=pb_response.hostname,
)

def to_proto(self):
return response(
time=pb_timestamp_from_datetime(self.time),
priority=priority.Value(self.priority.value),
source=source.Value(self.source.value),
rule=self.rule,
output=self.output,
output_fields=self.output_fields,
hostname=self.hostname,
)
4 changes: 4 additions & 0 deletions falco/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sys
import os

sys.path.append(os.path.abspath(os.path.dirname(__file__)))
Loading

0 comments on commit e1a980e

Please sign in to comment.