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

RTR Robustness Updates #205

Merged
merged 7 commits into from
Oct 17, 2024
Merged
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[flake8]
max-line-length = 100
extend-ignore = E203
37 changes: 33 additions & 4 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,56 @@ jobs:
codequality:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
# TODO: update this to 3.13 (final release) when this is released on 1 October 2024
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Poetry via pipx
run: pipx install poetry

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'

- name: Install dependencies
run: poetry install

- name: Lint package source with flake8
run: poetry run flake8 caracara/ --show-source --statistics
run: |
poetry run flake8 caracara/ --show-source --statistics
poetry run flake8 examples/ --show-source --statistics
poetry run flake8 tests/ --show-source --statistics

- name: Lint package source with pylint
if: success() || failure()
run: poetry run pylint caracara/
run: |
poetry run pylint caracara/
poetry run pylint examples/
poetry run pylint tests/

- name: Lint package docstrings and comments with pydocstyle
if: success() || failure()
run: poetry run pydocstyle caracara/
run: |
poetry run pydocstyle caracara/
poetry run pydocstyle examples/

- name: Lint imports with isort
if: success() || failure()
run: |
poetry run isort -c caracara/
poetry run isort -c examples/
poetry run isort -c tests/

- name: Lint package with black
if: success() || failure()
run: |
poetry run black -l 100 --check caracara/
poetry run black -l 100 --check examples/
poetry run black -l 100 --check tests/

- name: Analyse code for security issues with bandit
if: success() || failure()
run: |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ A few of the developer experience enhancements provided by the Caracara toolkit

Caracara supports all major Python packaging solutions. Instructions for [Poetry](https://python-poetry.org) and [Pip](https://pypi.org/project/pip/) are provided below.

Caracara supports Python versions that are still supported by the Python Software Foundation, i.e., **Python 3.8 and up**.

<details>
<summary><h3>Installing Caracara from PyPI using Poetry (Recommended!)</h3></summary>

Expand Down
1 change: 1 addition & 0 deletions caracara/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

Developer Toolkit for FalconPy
"""

__all__ = ["Client", "Policy"]

from caracara.client import Client
Expand Down
11 changes: 5 additions & 6 deletions caracara/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@
import logging

try:
from falconpy import (
OAuth2,
confirm_base_region,
confirm_base_url
)
from falconpy import OAuth2, confirm_base_region, confirm_base_url
except ImportError as no_falconpy:
raise SystemExit("The crowdstrike-falconpy library is not installed.") from no_falconpy

from caracara_filters import FQLGenerator

from caracara.common.interpolation import VariableInterpolator
from caracara.common.meta import user_agent_string
from caracara.common.module import ModuleMapper
Expand Down Expand Up @@ -117,7 +114,9 @@ def __init__( # pylint: disable=R0913,R0914,R0915

self.logger.info(
"Client ID: %s; Cloud: %s; Member CID: %s",
client_id, cloud_name, member_cid,
client_id,
cloud_name,
member_cid,
)
self.logger.debug("SSL verification is %s", ssl_verify)
self.logger.debug("Timeout: %s", str(timeout))
Expand Down
5 changes: 3 additions & 2 deletions caracara/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Caracara: Common functions and data imports."""

__all__ = [
"DEFAULT_DATA_BATCH_SIZE",
"FalconApiModule",
Expand All @@ -7,7 +8,7 @@
"user_agent_string",
]

from caracara.common.constants import SCROLL_BATCH_SIZE, DEFAULT_DATA_BATCH_SIZE
from caracara.common.policy_wrapper import Policy
from caracara.common.constants import DEFAULT_DATA_BATCH_SIZE, SCROLL_BATCH_SIZE
from caracara.common.meta import user_agent_string
from caracara.common.module import FalconApiModule
from caracara.common.policy_wrapper import Policy
27 changes: 15 additions & 12 deletions caracara/common/batching.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@
For paginated results that send back IDs, the pagination code file contains
the required code to pull this data down as quickly as possible.
"""

import concurrent.futures
import logging
import multiprocessing

from functools import partial
from threading import current_thread
from typing import Callable, Dict, List, Tuple

from caracara.common.constants import DEFAULT_DATA_BATCH_SIZE


BATCH_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -59,7 +58,9 @@ def batch_get_data(
BATCH_LOGGER.debug(str(lookup_ids))

# Divide the list of item IDs into a list of lists, each of size data_batch_size
batches = [lookup_ids[i:i+data_batch_size] for i in range(0, len(lookup_ids), data_batch_size)]
batches = [
lookup_ids[i : i + data_batch_size] for i in range(0, len(lookup_ids), data_batch_size)
]
BATCH_LOGGER.info("Divided the item IDs into %d batches", len(batches))

threads = batch_data_pull_threads()
Expand All @@ -72,7 +73,9 @@ def worker(
thread_name = current_thread().name
BATCH_LOGGER.info(
"%s | Batch worker started with a list of %d items. Function: %s",
thread_name, len(worker_lookup_ids), batch_func.__name__,
thread_name,
len(worker_lookup_ids),
batch_func.__name__,
)
body: Dict = batch_func(ids=worker_lookup_ids)["body"]
# Gracefully handle a lack of returned resources, usually as a result of an error
Expand Down Expand Up @@ -100,14 +103,14 @@ def worker(

resources_dict = {}
for resource in resources:
if 'id' in resource:
resources_dict[resource['id']] = resource
elif 'device_id' in resource:
resources_dict[resource['device_id']] = resource
elif 'child_cid' in resource:
resources_dict[resource['child_cid']] = resource
elif 'uuid' in resource:
resources_dict[resource['uuid']] = resource
if "id" in resource:
resources_dict[resource["id"]] = resource
elif "device_id" in resource:
resources_dict[resource["device_id"]] = resource
elif "child_cid" in resource:
resources_dict[resource["child_cid"]] = resource
elif "uuid" in resource:
resources_dict[resource["uuid"]] = resource
else:
raise KeyError("No ID field to build the dictionary from")

Expand Down
1 change: 1 addition & 0 deletions caracara/common/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Common constants to be shared throughout Caracara."""

from enum import Enum, EnumMeta

# Batch size of data downloaded via a multi-threaded data pull
Expand Down
35 changes: 19 additions & 16 deletions caracara/common/csdialog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Caracara: CrowdStrike Style Radio Dialog for Prompt Toolkit."""

from __future__ import annotations

from typing import Any, Sequence, TypeVar

from prompt_toolkit.application import Application
Expand All @@ -13,7 +15,6 @@
from prompt_toolkit.styles import BaseStyle, Style, merge_styles
from prompt_toolkit.widgets import Button, Dialog, Label, RadioList


# Prompt Toolkit uses _T for the generic type
_T = TypeVar("_T")

Expand Down Expand Up @@ -41,21 +42,23 @@ def _return_none() -> None:
get_app().exit()


CS_STYLE = Style.from_dict({
'button': 'fg:#1A1A1A bold',
'button.arrow': 'fg:#FC0000 bold',
'button.focused': 'fg:#FFFFFF bg:#2F8BAA',
'button.text': 'fg:#1A1A1A bold',
'dialog': 'bg:#58595B',
'dialog.body': 'bg:#F3F3F4',
'dialog.body label': 'fg:#FC0000',
'dialog frame.label': 'bg:#F3F4F4 #1A1A1A',
'dialog shadow': 'bg:#68696B',
'frame.label': 'fg:#FC0000',
'label': '#FC0000',
'radio-list': '#1A1A1A',
'radio-checked': 'fg:#2F8BAA',
})
CS_STYLE = Style.from_dict(
{
"button": "fg:#1A1A1A bold",
"button.arrow": "fg:#FC0000 bold",
"button.focused": "fg:#FFFFFF bg:#2F8BAA",
"button.text": "fg:#1A1A1A bold",
"dialog": "bg:#58595B",
"dialog.body": "bg:#F3F3F4",
"dialog.body label": "fg:#FC0000",
"dialog frame.label": "bg:#F3F4F4 #1A1A1A",
"dialog shadow": "bg:#68696B",
"frame.label": "fg:#FC0000",
"label": "#FC0000",
"radio-list": "#1A1A1A",
"radio-checked": "fg:#2F8BAA",
}
)


def csradiolist_dialog( # pylint: disable=too-many-arguments
Expand Down
16 changes: 9 additions & 7 deletions caracara/common/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Caracara module-agnostic decorators."""

from functools import wraps
from inspect import signature
from typing import Callable
Expand All @@ -10,10 +11,10 @@ def platform_name_check(func: Callable):
"""Decorate a function to ensure that a platform_name argument is within the specified list."""
# Load the function's signature, and confirm that the platform_name parameter has been added
sig = signature(func)
if 'platform_name' not in sig.parameters:
raise ValueError(f'The function {func.__name__} does not have a platform_name parameter')
if "platform_name" not in sig.parameters:
raise ValueError(f"The function {func.__name__} does not have a platform_name parameter")

if 'self' not in sig.parameters:
if "self" not in sig.parameters:
raise ValueError(
f"The function {func.__name__} must be a class function with a self parameter"
)
Expand All @@ -25,10 +26,10 @@ def wrapper(*args, **kwargs):
# Apply any default parameters (e.g., platform_name=None where this is not specified)
_args.apply_defaults()
# Get references to the self and platform_name arguments for logging and processing purposes
self = _args.arguments['self']
platform_name = _args.arguments['platform_name']
self = _args.arguments["self"]
platform_name = _args.arguments["platform_name"]

if hasattr(self, 'logger'):
if hasattr(self, "logger"):
self.logger.debug(f"Entering filter_string wrapper for function {func.__name__}")

if platform_name and isinstance(platform_name, str):
Expand All @@ -41,9 +42,10 @@ def wrapper(*args, **kwargs):
# If we get this far, either the platform_name given is reasonable, or it was None (which
# is reasonable if the function signature allows this)

if hasattr(self, 'logger'):
if hasattr(self, "logger"):
self.logger.debug("Exiting decorator")

# Return to the original function
return func(*_args.args, **_args.kwargs)

return wrapper
Loading