Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1.0.0 release #10

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Add the SDK to your project by following the [installation](#installation) instr
then create your `handler.py`:

```python
import logging
from crowdstrike.foundry.function import (
APIError,
Request,
Expand All @@ -35,7 +36,8 @@ func = Function.instance() # *** (1) ***


@func.handler(method='POST', path='/create') # *** (2) ***
def on_create(request: Request, config: [dict[str, any], None]) -> Response: # *** (3), (4) ***
def on_create(request: Request, config: [dict[str, any], None],
logger: logging.Logger) -> Response: # *** (3), (4), (5) ***
if len(request.body) == 0:
return Response(
code=400,
Expand All @@ -46,14 +48,30 @@ def on_create(request: Request, config: [dict[str, any], None]) -> Response: #
# do something useful
#####

return Response( # *** (5) ***
return Response( # *** (6) ***
body={'hello': 'world'},
code=200,
)


@func.handler(method='PUT', path='/update')
def on_update(request: Request) -> Response: # *** (7) ***
# do stuff
return Response(
# ...snip...
)


@func.handler(method='DELETE', path='/foo')
def on_delete(request: Request, config: [dict[str, any], None]) -> Response: # *** (8) ***
# do stuff
return Response(
# ...snip...
)


if __name__ == '__main__':
func.run() # *** (6) ***
func.run() # *** (9) ***
```

1. `Function`: The `Function` class wraps the Foundry Function implementation.
Expand All @@ -75,14 +93,22 @@ if __name__ == '__main__':
3. `url`: The request path relative to the function as a string.
4. `method`: The request HTTP method or verb.
5. `access_token`: Caller-supplied access token.
5. Return from a `@handler` function: Returns a `Response` object.
5. `logger`: Unless there is specific reason not to, the function author should use the `Logger` provided to the
function.
When deployed, the supplied `Logger` will be formatted in a custom manner and will have fields injected to assist
with working against our internal logging infrastructure.
Failure to use the provided `Logger` can thus make triage more difficult.
6. Return from a `@handler` function: Returns a `Response` object.
The `Response` object contains fields `body` (payload of the response as a `dict`),
`code` (an `int` representing an HTTP status code),
`errors` (a list of any `APIError`s), and `header` (a `dict[str, list[str]]` of any special HTTP headers which
should be present on the response).
If no `code` is provided but a list of `errors` is, the `code` will be derived from the greatest positive valid HTTP
code present on the given `APIError`s.
6. `func.run()`: Runner method and general starting point of execution.
7. `on_update(request: Request)`: If only one argument is provided, only a `Request` will be provided.
8. `on_delete(request: Request, config: [dict[str, any], None])`: If two arguments are provided, a `Request` and config
will be provided.
9. `func.run()`: Runner method and general starting point of execution.
Calling `run()` causes the `Function` to finish initializing and start executing.
Any code declared following this method may not necessarily be executed.
As such, it is recommended to place this as the last line of your script.
Expand Down Expand Up @@ -110,21 +136,18 @@ curl -X POST 'http://localhost:8081' \
}'
```

## Convenience Functionality 🧰
## Working with `falconpy`

### `falconpy`
Function authors should import `falconpy` explicitly as a requirement in their project when needed.

Foundry Function Python ships with [falconpy](https://github.com/CrowdStrike/falconpy) pre-integrated and a convenience
constructor.
While it is not strictly necessary to use the convenience function, it is recommended.
### General usage

**Important:** Create a new instance of each `falconpy` client you want on each request.

```python
# omitting other imports
from falconpy.alerts import Alerts
from falconpy.event_streams import EventStreams
from crowdstrike.foundry.function import falcon_client, Function
from crowdstrike.foundry.function import cloud, Function

func = Function.instance()

Expand All @@ -136,8 +159,7 @@ def endpoint(request, config):
# !!! create a new client instance on each request !!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

alerts_client = falcon_client(Alerts)
event_streams_client = falcon_client(EventStreams)
falconpy_alerts = Alerts(access_token=request.access_token, base_url=cloud())

# ... omitting other code ...
```
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
build>=1.0.3
crowdstrike-falconpy>=1.3.2
pytest>=7.4.2
urllib3>=1.26.16,<2.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
SETUP_REQUIRES = [
'setuptools',
]
VERSION = '0.6.0'
VERSION = '1.0.0'


def main():
Expand Down
17 changes: 16 additions & 1 deletion src/crowdstrike/foundry/function/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from crowdstrike.foundry.function.falconpy import falcon_client
from crowdstrike.foundry.function.model import *


Expand Down Expand Up @@ -106,3 +105,19 @@ def call(func):
))

return call


def cloud() -> str:
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Retrieves a FalconPy-compatible identifier which identifies the cloud in which this function is running.
:return: Cloud in which this function is executing.
"""
import os

_default = 'auto'
c = os.environ.get('CS_CLOUD', _default)
c = c.lower().replace('-', '').strip()
if c == '':
c = _default

return c
37 changes: 0 additions & 37 deletions src/crowdstrike/foundry/function/falconpy.py

This file was deleted.

4 changes: 3 additions & 1 deletion src/crowdstrike/foundry/function/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ class Request:
access_token: str = field(default='')
body: Dict[str, any] = field(default_factory=lambda: {})
context: Dict[str, any] = field(default_factory=lambda: {})
fn_id: str = field(default='')
fn_version: int = field(default=0)
method: str = field(default='')
params: RequestParams = field(default_factory=lambda: RequestParams())
trace_id: str = field(default='')
url: str = field(default='')
cloud: str = field(default='')


@dataclass
Expand Down
24 changes: 19 additions & 5 deletions src/crowdstrike/foundry/function/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from crowdstrike.foundry.function.model import FDKException, Request, Response
from dataclasses import dataclass
from http.client import BAD_REQUEST, METHOD_NOT_ALLOWED, NOT_FOUND, SERVICE_UNAVAILABLE
from inspect import signature
from logging import Logger
from typing import Callable


Expand All @@ -16,8 +18,9 @@ class Router:
Serves to route function requests to the appropriate handler functions.
"""

def __init__(self, config):
def __init__(self, config, logger: [Logger, None] = None):
self._config = config
self._logger = logger
self._routes = {}

def route(self, req: Request) -> Response:
Expand All @@ -35,15 +38,26 @@ def route(self, req: Request) -> Response:
message="Unsupported method format, expects string: {}".format(req.method))

methods_for_url = self._routes.get(req.url, None)
req_method = req.method.strip().upper()
if methods_for_url is None:
raise FDKException(code=NOT_FOUND, message="Not Found: {}".format(req.url))
raise FDKException(code=NOT_FOUND, message="Not Found: {} {}".format(req_method, req.url))

req_method = req.method.strip().upper()
r = methods_for_url.get(req_method, None)
if r is None:
raise FDKException(code=METHOD_NOT_ALLOWED, message="Method Not Allowed: {}".format(req_method))
raise FDKException(code=METHOD_NOT_ALLOWED, message="Method Not Allowed: {} at endpoint".format(req_method))

return self._call_route(r, req)

def _call_route(self, route: Route, req: Request):
f = route.func
len_params = len(signature(f).parameters)

return r.func(req, self._config)
# We'll make this more flexible in the future if needed.
if len_params == 3:
return f(req, self._config, self._logger)
if len_params == 2:
return f(req, self._config)
return f(req)

def register(self, r: Route):
"""
Expand Down
Loading
Loading