├── .flake8
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── codeql-analysis.yml
│ ├── docs.yml
│ ├── python-lint.yml
│ ├── python-release.yml
│ ├── python-test-quarantine.yml
│ └── python-test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── DEVELOPER_GUIDELINES.md
├── LICENSE
├── README.md
├── docs-source
├── .gitignore
├── conf.py
├── index.rst
└── static
│ ├── authentication.rst
│ ├── installation.rst
│ ├── overview.rst
│ ├── timestamps.rst
│ ├── troubleshooting.rst
│ └── usage.rst
├── docs
├── .gitignore
└── README.md
├── examples
├── example_active_container_vulnerabilities.py
├── example_alert_channels.py
├── example_alerts.py
├── example_audit_logs.py
├── example_cloud_accounts.py
├── example_cloud_activities.py
├── example_export.py
├── example_inventory.py
├── example_query_policy.py
├── example_reports.py
├── example_schemas.py
├── example_syscall_query_policy.py
├── example_team_users.py
├── example_tokens.py
└── example_vulnerabilities.py
├── jupyter
├── MANIFEST.in
├── README.md
├── devtools
│ └── deploy_to_container.sh
├── docker
│ ├── docker-build.yml
│ ├── docker-compose.yml
│ └── docker_build
│ │ ├── 00-import.py
│ │ ├── Dockerfile
│ │ ├── custom.css
│ │ ├── jupyter_notebook_config.py
│ │ ├── lacework
│ │ ├── README.md
│ │ ├── lacework.yaml
│ │ └── main.js
│ │ ├── logo.png
│ │ └── snippets.json
├── laceworkjupyter
│ ├── __init__.py
│ ├── accessors.py
│ ├── config.py
│ ├── decorators.py
│ ├── features
│ │ ├── __init__.py
│ │ ├── cache.py
│ │ ├── client.py
│ │ ├── date.py
│ │ ├── filters.yaml
│ │ ├── helper.py
│ │ ├── hunt.py
│ │ ├── join.yaml
│ │ ├── mitre.py
│ │ ├── mitre.yaml
│ │ ├── policies.py
│ │ ├── query.py
│ │ ├── query.yaml
│ │ ├── query_builder.py
│ │ ├── tables.yaml
│ │ └── utils.py
│ ├── helper.py
│ ├── manager.py
│ ├── plugins
│ │ ├── __init__.py
│ │ ├── alert_rules.py
│ │ ├── alerts.py
│ │ ├── datasources.py
│ │ └── utils.py
│ ├── utils.py
│ └── version.py
├── notebooks
│ ├── colab_sample.ipynb
│ └── sample_notebook.ipynb
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
│ ├── __init__.py
│ └── laceworkjupyter
│ ├── __init__.py
│ ├── test_accessors.py
│ └── test_utils.py
├── laceworksdk
├── __init__.py
├── api
│ ├── __init__.py
│ ├── base_endpoint.py
│ ├── crud_endpoint.py
│ ├── read_endpoint.py
│ ├── search_endpoint.py
│ └── v2
│ │ ├── __init__.py
│ │ ├── activities.py
│ │ ├── agent_access_tokens.py
│ │ ├── agent_info.py
│ │ ├── alert_channels.py
│ │ ├── alert_profiles.py
│ │ ├── alert_rules.py
│ │ ├── alerts.py
│ │ ├── audit_logs.py
│ │ ├── cloud_accounts.py
│ │ ├── cloud_activities.py
│ │ ├── configs.py
│ │ ├── container_registries.py
│ │ ├── contract_info.py
│ │ ├── data_export_rules.py
│ │ ├── datasources.py
│ │ ├── entities.py
│ │ ├── events.py
│ │ ├── inventory.py
│ │ ├── organization_info.py
│ │ ├── policies.py
│ │ ├── policy_exceptions.py
│ │ ├── queries.py
│ │ ├── report_definitions.py
│ │ ├── report_rules.py
│ │ ├── reports.py
│ │ ├── resource_groups.py
│ │ ├── schemas.py
│ │ ├── team_members.py
│ │ ├── team_users.py
│ │ ├── user_groups.py
│ │ ├── user_profile.py
│ │ ├── vulnerabilities.py
│ │ ├── vulnerability_exceptions.py
│ │ └── vulnerability_policies.py
├── config.py
├── exceptions.py
└── http_session.py
├── poetry.lock
├── pyproject.toml
├── templates
└── .release_notes.md.j2
└── tests
├── __init__.py
├── api
├── __init__.py
├── test_base_endpoint.py
├── test_crud_endpoint.py
├── test_laceworksdk.py
├── test_read_endpoint.py
├── test_search_endpoint.py
└── v2
│ ├── __init__.py
│ ├── test_activities.py
│ ├── test_agent_access_tokens.py
│ ├── test_agent_info.py
│ ├── test_alert_channels.py
│ ├── test_alert_profiles.py
│ ├── test_alert_rules.py
│ ├── test_alerts.py
│ ├── test_audit_logs.py
│ ├── test_cloud_accounts.py
│ ├── test_cloud_activities.py
│ ├── test_configs.py
│ ├── test_container_registries.py
│ ├── test_contract_info.py
│ ├── test_data_export_rules.py
│ ├── test_datasources.py
│ ├── test_entities.py
│ ├── test_events.py
│ ├── test_inventory.py
│ ├── test_organization_info.py
│ ├── test_policies.py
│ ├── test_policy_exceptions.py
│ ├── test_queries.py
│ ├── test_report_definitions.py
│ ├── test_report_rules.py
│ ├── test_reports.py
│ ├── test_resource_groups.py
│ ├── test_schemas.py
│ ├── test_team_members.py
│ ├── test_team_users.py
│ ├── test_user_groups.py
│ ├── test_user_profile.py
│ ├── test_vulnerabilities.py
│ ├── test_vulnerability_exceptions.py
│ └── test_vulnerability_policies.py
├── conftest.py
├── environment.py
└── test_laceworksdk.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | inline-quotes = double
3 | max-line-length = 100
4 | ignore =
5 | # E501 module level import not at top of file
6 | E501,
7 | exclude =
8 | .eggs/,
9 | .git/,
10 | .idea/,
11 | .venv/,
12 | .vscode/,
13 | __pycache__,
14 | build/,
15 | dist/,
16 | venv/,
17 | example.py,
18 | **/version.py
19 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @lacework/eng-product-platform
2 | /jupyter/ @kiddinn
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'bug:'
5 | labels: bug
6 | ---
7 |
8 | **Describe the bug**
9 | A clear and concise description of what the bug is.
10 |
11 | **To Reproduce**
12 | Steps to reproduce the behavior:
13 | 1. Run cmd '...'
14 | 2. See error
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Please complete the following information:**
23 | - OS: [e.g. macOS Big Sur 11.5.2 ]
24 | - Version [e.g. v0.15.0]
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'feat: '
5 | labels: 'feat'
6 | ---
7 |
8 | # Feature Request
9 |
10 | **Describe the Feature Request**
11 | A clear and concise description of what the feature request is. Please include if your feature request is related to a problem
12 |
13 | **Is your feature request related to a problem? Please describe**
14 | Problems related that made you consider this feature request
15 |
16 | **Describe Preferred Solution**
17 | A clear and concise description of what you want to happen and alternatives
18 |
19 | **Additional Context**
20 | List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, etc.
21 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL Code Scanning"
2 |
3 | on:
4 | push:
5 | branches: [master, main]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [master, main]
9 | schedule:
10 | - cron: '0 0 * * 6'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | # Override automatic language detection by changing the below list
21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
22 | language: ['python']
23 | # Learn more...
24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v2
29 |
30 | # Initializes the CodeQL tools for scanning.
31 | - name: Initialize CodeQL
32 | uses: github/codeql-action/init@v1
33 | with:
34 | languages: ${{ matrix.language }}
35 | # If you wish to specify custom queries, you can do so here or in a config file.
36 | # By default, queries listed here will override any specified in a config file.
37 | # Prefix the list here with "+" to use these queries and those in the config file.
38 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
39 |
40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
41 | # If this step fails, then you should remove it and run the build manually (see below)
42 | - name: Autobuild
43 | uses: github/codeql-action/autobuild@v1
44 |
45 | # ℹ️ Command-line programs to run using the OS shell.
46 | # 📚 https://git.io/JvXDl
47 |
48 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
49 | # and modify them (or add more) to build your code if your project
50 | # uses a compiled language
51 |
52 | #- run: |
53 | # make bootstrap
54 | # make release
55 |
56 | - name: Perform CodeQL Analysis
57 | uses: github/codeql-action/analyze@v1
58 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs-website
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | # Build the documentation and upload the static HTML files as an artifact.
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - name: Install poetry
19 | run: |
20 | pipx install poetry
21 |
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v4
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | cache: 'poetry'
27 |
28 | - name: Install dependencies
29 | run: |
30 | poetry check
31 | poetry lock --no-update
32 | poetry install --verbose
33 |
34 | - run: poetry run poe docs
35 |
36 | - uses: actions/upload-pages-artifact@v1
37 | with:
38 | path: docs/html
39 |
40 | # Deploy the artifact to GitHub pages.
41 | deploy:
42 | needs: build
43 | runs-on: ubuntu-latest
44 | permissions:
45 | pages: write
46 | id-token: write
47 | environment:
48 | name: github-pages
49 | url: ${{ steps.deployment.outputs.page_url }}
50 | steps:
51 | - id: deployment
52 | uses: actions/deploy-pages@v1
53 |
--------------------------------------------------------------------------------
/.github/workflows/python-lint.yml:
--------------------------------------------------------------------------------
1 | name: Run Python Linting/Docstring Linting
2 |
3 | on:
4 | push:
5 | branches:
6 | - "*"
7 | pull_request:
8 | branches:
9 | - main
10 | schedule:
11 | - cron: "0 8 * * *"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | python-version: ["3.11"]
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Install poetry
27 | run: |
28 | pipx install poetry
29 |
30 | - name: Set up Python ${{ matrix.python-version }}
31 | uses: actions/setup-python@v4
32 | with:
33 | python-version: ${{ matrix.python-version }}
34 | cache: 'poetry'
35 |
36 | - name: Install dependencies
37 | run: |
38 | poetry check
39 | poetry lock --no-update
40 | poetry install --verbose
41 |
42 | - name: Lint Docstrings with ruff
43 | run: |
44 | poetry run poe lint-docstrings
45 |
46 | - name: Lint with flake8
47 | run: |
48 | # stop the build if there are Python syntax errors or undefined names
49 | poetry run poe lint
50 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
51 | poetry run flake8 . --exclude "jupyter" --count --exit-zero --max-complexity=10 --statistics
52 |
--------------------------------------------------------------------------------
/.github/workflows/python-release.yml:
--------------------------------------------------------------------------------
1 | name: Semantic Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | bump_major:
7 | required: false
8 | type: choice
9 | description: should the major version be incremented
10 | default: 'false'
11 | options:
12 | - true
13 | - false
14 |
15 | jobs:
16 | release:
17 | runs-on: ubuntu-latest
18 | concurrency: release
19 | permissions:
20 | id-token: write
21 | contents: write
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 | token: ${{ secrets.TOKEN }}
28 |
29 | - name: Install poetry
30 | run: |
31 | pipx install poetry
32 |
33 | - name: Set up Python 3.11
34 | uses: actions/setup-python@v4
35 | with:
36 | python-version: 3.11
37 | cache: 'poetry'
38 |
39 | - name: Install dependencies
40 | run: |
41 | poetry check
42 | poetry lock --no-update
43 | poetry install --verbose
44 |
45 | - name: Python Semantic Major Release
46 | if: ${{ inputs.bump_major == 'true' }}
47 | run: |
48 | git config --global user.name "Lacework Inc."
49 | git config --global user.email tech-ally@lacework.net
50 | echo "${{ secrets.GPG_SECRET_KEY }}" | base64 --decode | gpg --import --no-tty --batch --yes
51 | git config --global user.signingkey ${{ secrets.GPG_SIGNING_KEY }}
52 | git config commit.gpgsign true
53 | git config --global tag.gpgSign true
54 | poetry run semantic-release version --major
55 | poetry run semantic-release publish
56 | env:
57 | GH_TOKEN: ${{secrets.TOKEN}}
58 |
59 |
60 | - name: Python Semantic Release
61 | if: ${{ inputs.bump_major == 'false' }}
62 | run: |
63 | git config --global user.name "Lacework Inc."
64 | git config --global user.email tech-ally@lacework.net
65 | echo "${{ secrets.GPG_SECRET_KEY }}" | base64 --decode | gpg --import --no-tty --batch --yes
66 | git config --global user.signingkey ${{ secrets.GPG_SIGNING_KEY }}
67 | git config commit.gpgsign true
68 | git config --global tag.gpgSign true
69 | poetry run semantic-release version
70 | poetry run semantic-release publish
71 | env:
72 | GH_TOKEN: ${{secrets.TOKEN}}
73 |
74 |
75 | - name: Publish to PyPi
76 | run: |
77 | poetry publish -vvv --username $PYPI_USERNAME --password $PYPI_PASSWORD
78 | env:
79 | PYPI_USERNAME: __token__
80 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
81 |
--------------------------------------------------------------------------------
/.github/workflows/python-test-quarantine.yml:
--------------------------------------------------------------------------------
1 | name: Run Python Quarantine Tests
2 |
3 | on:
4 | schedule:
5 | - cron: "0 8 * * 1"
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Install poetry
21 | run: |
22 | pipx install poetry
23 |
24 | - name: Set up Python ${{ matrix.python-version }}
25 | uses: actions/setup-python@v2
26 | with:
27 | python-version: ${{ matrix.python-version }}
28 |
29 | - name: Install dependencies
30 | run: |
31 | poetry check
32 | poetry lock --no-update
33 | poetry install --verbose
34 |
35 | - name: Test SDK quarantine tests
36 | run: |
37 | poetry run poe quarantine_test
38 | env:
39 | LW_ACCOUNT: ${{ secrets.LW_ACCOUNT }}
40 | LW_SUBACCOUNT: ${{ secrets.LW_SUBACCOUNT }}
41 | LW_API_KEY: ${{ secrets.LW_API_KEY }}
42 | LW_API_SECRET: ${{ secrets.LW_API_SECRET }}
43 | LW_BASE_DOMAIN: ${{ secrets.LW_BASE_DOMAIN }}
44 |
--------------------------------------------------------------------------------
/.github/workflows/python-test.yml:
--------------------------------------------------------------------------------
1 | name: Run Python Tests/Linting
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | schedule:
11 | - cron: "0 8 * * *"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Install poetry
27 | run: |
28 | pipx install poetry
29 |
30 | - name: Set up Python ${{ matrix.python-version }}
31 | uses: actions/setup-python@v4
32 | with:
33 | python-version: ${{ matrix.python-version }}
34 | cache: 'poetry'
35 |
36 | - name: Install dependencies
37 | run: |
38 | poetry check
39 | poetry lock --no-update
40 | poetry install --verbose
41 |
42 | - name: Lint with flake8
43 | run: |
44 | # stop the build if there are Python syntax errors or undefined names
45 | poetry run poe lint
46 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
47 | poetry run flake8 . --exclude "jupyter" --count --exit-zero --max-complexity=10 --statistics
48 |
49 | - name: Test SDK with pytest
50 | run: |
51 | poetry run poe test
52 | env:
53 | LW_ACCOUNT: ${{ secrets.LW_ACCOUNT }}
54 | LW_SUBACCOUNT: ${{ secrets.LW_SUBACCOUNT }}
55 | LW_API_KEY: ${{ secrets.LW_API_KEY }}
56 | LW_API_SECRET: ${{ secrets.LW_API_SECRET }}
57 | LW_BASE_DOMAIN: ${{ secrets.LW_BASE_DOMAIN }}
58 | - name: Report Status
59 | if: github.ref_name == 'main'
60 | uses: ravsamhq/notify-slack-action@v2
61 | with:
62 | status: ${{ job.status }}
63 | notify_when: "failure"
64 | notification_title: "{workflow} has {status_message}"
65 | message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>"
66 | footer: "Linked Repo <{repo_url}|{repo}> | <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Workflow>"
67 | env:
68 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local Files
2 | build/
3 | local/
4 |
5 | # Environment Variables
6 | .env
7 |
8 | # Virtual Environments
9 | .venv/
10 | venv/
11 | env/
12 |
13 | # Test artifacts
14 | .cache/
15 | .pytest_cache/
16 | example.py
17 |
18 | # Byte-compiled / optimized files
19 | *.py[cod]
20 |
21 | # Build artifacts
22 | dist/
23 | docs/_build/
24 | .eggs
25 | *.egg-info/
26 | .DS_Store
27 |
28 | # Code editors
29 | .vscode
30 | .idea
31 |
32 | # Pyenv local
33 | .python-version
34 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v4.4.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: end-of-file-fixer
9 | - id: check-yaml
10 | - id: check-json
11 | - id: check-toml
12 | - id: check-merge-conflict
13 | - id: check-added-large-files
14 | - repo: https://github.com/commitizen-tools/commitizen
15 | rev: v2.42.1
16 | hooks:
17 | - id: commitizen
18 | - id: commitizen-branch
19 | stages: [push]
20 | - repo: https://github.com/python-poetry/poetry
21 | rev: '1.6.1'
22 | hooks:
23 | - id: poetry-lock
24 | - id: poetry-check
25 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing to the Lacework Go-sdk
3 |
4 | ### Table Of Contents
5 |
6 | * [Before getting started?](#before-getting-started)
7 |
8 | * [How to contribute](#how-to-contribute)
9 | * [Reporting Bugs](#reporting-bugs)
10 | * [Feature Requests](#feature-requests)
11 | * [Pull Requests](#pull-requests)
12 |
13 | * [Developer Guidelines](/DEVELOPER_GUIDELINES.md)
14 |
15 |
16 | ## Before getting started
17 |
18 | Read the [README.md](https://github.com/lacework/python-sdk/blob/main/README.md)
19 |
20 | ### Poetry
21 |
22 | ```
23 | pyenv install 3.8
24 | pyenv virtualenv 3.8 python-sdk
25 | pyenv local python-sdk
26 |
27 | poetry install
28 | poetry run pytest ...
29 |
30 | pre-commit install --hook-type commit-msg --hook-type pre-push
31 | ```
32 |
33 | #### Install Dependencies
34 |
35 |
36 | https://python-poetry.org/docs/basic-usage/#installing-dependencies
37 |
38 | #### Run
39 |
40 | ```poetry run```
41 |
42 | https://python-poetry.org/docs/basic-usage/#installing-dependencies
43 |
44 |
45 | ## How to contribute
46 | There are 3 ways that community members can help contribute to the Lacework Python SDK. Reporting any issues you may find in the functionality or documentation. Or if you believe some functionality should exist within the SDK you can make a feature request. Finally, if you've gone one step further and made the changes to submit for a pull request.
47 |
48 | ### Reporting Bugs
49 |
50 | Ensure the issue you are raising has not already been created under [issues](https://github.com/lacework/python-sdk/issues).
51 |
52 | If no current issue addresses the problem, open a new [issue](https://github.com/lacework/python-sdk/issues/new).
53 | Include as much relevant information as possible. See the [bug template](https://github.com/lacework/python-sdk/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) for help on creating a new issue.
54 |
55 | ### Feature Requests
56 |
57 | If you wish to submit a request to add new functionality or an improvement to the go-sdk then use the the [feature request](https://github.com/lacework/python-sdk/blob/main/.github/ISSUE_TEMPLATE/feature_request.md) template to
58 | open a new [issue](https://github.com/lacework/python-sdk/issues/new)
59 |
60 | ### Pull Requests
61 |
62 | When submitting a pull request follow the [commit message standard](DEVELOPER_GUIDELINES.md#commit-message-standard).
63 |
64 |
65 | Thanks,
66 | Project Maintainers
67 |
--------------------------------------------------------------------------------
/DEVELOPER_GUIDELINES.md:
--------------------------------------------------------------------------------
1 | ## Developer Guidelines
2 |
3 | ## Signed Commits
4 | Signed commits are required for any contribution to this project. Please see Github's documentation on configuring signed commits, [tell git about your signing key](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification/telling-git-about-your-signing-key) and [signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
5 |
6 | ## Commit message standard
7 |
8 | The format is:
9 | ```
10 | type(scope): subject
11 |
12 | BODY
13 |
14 | FOOTER
15 | ```
16 |
17 | Example commit message:
18 | ```
19 | feat(cli): add --api_token global flag
20 |
21 | This new flag will replace the use of `api_key` and `api_secret` so that
22 | users can run the Lacework CLI only with an access token and their account:
23 |
24 | lacework cloud-account list --api_token _secret123 -a mycompany
25 |
26 | Closes https://github.com/lacework/python-sdk/issues/282
27 | ```
28 |
29 | Each commit message consists of a header, body, and footer. The header with the type and subject are mandatory, the scope is optional.
30 | When writing a commit message try and limit each line of the commit to a max of 80 characters, so it can be read easily.
31 |
32 | ### Type
33 |
34 | Allowed `type` valued.
35 |
36 | | Type | Description |
37 | | ----- | ----------- |
38 | | feat: | A new feature you're adding |
39 | | fix: | A bug fix |
40 | | style: | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) |
41 | | refactor: | A code change that neither fixes a bug nor adds a feature |
42 | | test: | Everything related to testing |
43 | | docs: | Everything related to documentation |
44 | | chore: | Regular code maintenance |
45 | | build: | Changes that affect the build |
46 | | ci: | Changes to our CI configuration files and scripts |
47 | | perf: | A code change that improves performance |
48 |
49 | ### Scope
50 | The optional scope refers to the section that this commit belongs to, for example, changing a specific component or service, a directive, pipes, etc.
51 | Think about it as an indicator that will let the developers know at first glance what section of your code you are changing.
52 |
53 | A few good examples are:
54 |
55 | * feat(client):
56 | * docs(pipes):
57 | * chore(tests):
58 | * ci(directive):
59 |
60 | ### Subject
61 | The subject should contain a short description of the change, and written in present-tense, for example, use "add" and not "added", or "change" and not "changed".
62 | I like to fill this sentence below to understand what should I put as my description of my change:
63 |
64 | If applied, this commit will ________________________________________.
65 |
66 | ### Body
67 | The body should contain a longer description of the change, try not to repeat the subject and keep it in the present tense as above.
68 | Put as much context as you think it is needed, don’t be shy and explain your thought process, limitations, ideas for new features or fixes, etc.
69 |
70 | ### Footer
71 | The footer is used to reference issues, pull requests or breaking changes, for example, "Fixes ticket #123".
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Lacework, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs-source/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 | !.gitignore
3 | !conf.py
4 | !index.rst
5 | !static/
6 |
7 |
--------------------------------------------------------------------------------
/docs-source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | import os
7 | import sys
8 |
9 |
10 | #sys.path.insert(0, os.path.abspath('../laceworksdk'))
11 |
12 | # -- Project information -----------------------------------------------------
13 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
14 |
15 | project = 'Lacework Python SDK'
16 | copyright = '2024, Lacework'
17 | author = 'Jon Stewart, Tim MacDonald'
18 |
19 | # -- General configuration ---------------------------------------------------
20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
21 |
22 | extensions = ['sphinx.ext.napoleon',
23 | 'sphinx.ext.autodoc',
24 | 'autoapi.extension',
25 | 'sphinx.ext.doctest',
26 | 'sphinx.ext.todo',
27 | 'sphinx.ext.viewcode',
28 | 'sphinx.ext.githubpages']
29 |
30 | autoapi_dirs = ['../laceworksdk']
31 | autoapi_type = "python"
32 | autoapi_options = [
33 | "members",
34 | "show-inheritance",
35 | "show-module-summary",
36 | "imported-members",
37 | "inherited-members",
38 | "no-private-members"
39 | ]
40 | autodoc_default_options = {"members": True, "show-inheritance": True}
41 | autodoc_inherit_docstrings = True
42 | # Napoleon settings
43 | napoleon_google_docstring = True
44 | napoleon_numpy_docstring = False
45 | napoleon_include_init_with_doc = True
46 | napoleon_include_private_with_doc = False
47 | napoleon_include_special_with_doc = False
48 | napoleon_use_admonition_for_examples = False
49 | napoleon_use_admonition_for_notes = False
50 | napoleon_use_admonition_for_references = False
51 | napoleon_use_ivar = False
52 | napoleon_use_param = True
53 | napoleon_use_rtype = True
54 |
55 | templates_path = ['_templates']
56 | exclude_patterns = []
57 |
58 |
59 |
60 | # -- Options for HTML output -------------------------------------------------
61 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
62 |
63 | html_theme = 'sphinx_rtd_theme'
64 | html_static_path = ['_static']
65 | html_logo = 'https://techally-content.s3-us-west-1.amazonaws.com/public-content/lacework_logo_full.png'
66 | html_theme_options = {
67 | 'display_version': True,
68 | 'prev_next_buttons_location': 'bottom',
69 | 'style_external_links': False,
70 | 'vcs_pageview_mode': '',
71 | 'style_nav_header_background': 'white',
72 | # Toc options
73 | 'collapse_navigation': False,
74 | 'sticky_navigation': True,
75 | 'navigation_depth': -1,
76 | 'includehidden': False,
77 | 'titles_only': False
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/docs-source/index.rst:
--------------------------------------------------------------------------------
1 |
2 | Welcome to Lacework Python SDK's documentation!
3 | ===============================================
4 |
5 | .. toctree::
6 | :maxdepth: 5
7 |
8 | static/overview.rst
9 | static/installation.rst
10 | static/authentication.rst
11 | static/usage.rst
12 | static/timestamps.rst
13 | static/troubleshooting.rst
14 | autoapi/index
15 |
16 |
17 |
18 |
19 | Indices and tables
20 | ==================
21 |
22 | * :ref:`genindex`
23 | * :ref:`modindex`
24 | * :ref:`search`
25 |
--------------------------------------------------------------------------------
/docs-source/static/installation.rst:
--------------------------------------------------------------------------------
1 | ================================
2 | Lacework Python SDK Installation
3 | ================================
4 |
5 | The Lacework Python SDK is published on `PyPi `_ as ``laceworksdk``
6 |
7 | Installation and updates are managed using ``pip3``
8 |
9 | .. code-block::
10 | :caption: Installation
11 |
12 | pip3 install laceworksdk
13 |
14 | .. code-block::
15 | :caption: Update
16 |
17 | pip3 install laceworksdk --upgrade
18 |
19 |
--------------------------------------------------------------------------------
/docs-source/static/overview.rst:
--------------------------------------------------------------------------------
1 | =======================
2 | The Lacework Python SDK
3 | =======================
4 | `A Python module for interacting with the` `Lacework Cloud Security Platform `_
5 |
6 | The Lacework Python SDK is an interface to the `Lacework
7 | API `_ with a few
8 | additional quality of life features added in. It supports
9 | all currently maintained versions of Python 3.
10 |
11 | The Lacework Python SDK is an open source project and the source
12 | code is available on Github in the `Lacework Python SDK repo `_.
--------------------------------------------------------------------------------
/docs-source/static/timestamps.rst:
--------------------------------------------------------------------------------
1 | ========================================
2 | Lacework Python SDK Timestamp Generation
3 | ========================================
4 |
5 | For all "search" methods Lacework requires ``start_time`` and ``end_time`` arguments which
6 | are used to specify the search window. Additionally, some "get" methods also require them.
7 | These must be specified as strings in the following format:
8 |
9 | ``"%Y-%m-%dT%H:%M:%S%z"``
10 |
11 | Example:
12 |
13 | ``"2024-01-08T22:34:10+0000"``
14 |
15 | You are free to generate these strings however you like, but you may find it useful to
16 | use the following function (or something similar).
17 |
18 | .. code-block::
19 | :caption: Timestamp Generation Function
20 |
21 | from datetime import datetime, timedelta, timezone
22 |
23 | def generate_time_string(delta_days=0, delta_hours=0, delta_minutes=0, delta_seconds=0) -> str:
24 | return (datetime.now(timezone.utc) - timedelta(days=delta_days, hours=delta_hours, minutes=delta_minutes, seconds=delta_seconds)).strftime("%Y-%m-%dT%H:%M:%SZ")
25 |
26 |
27 | This will allow you to generate time stamps relative to "now" easily. For Example:
28 |
29 | .. code-block::
30 | :caption: Generating Timestamp
31 |
32 | right_now = generate_time_string()
33 |
34 | twelve_hours_ago = generate_time_string(delta_hours=12)
35 |
36 | one_day_ago = generate_time_string(delta_days=1)
37 |
38 | thirty_six_and_a_half_hours_ago = generate_time_string(delta_days=1,
39 | delta_hours=12,
40 | delta_minutes=30)
41 |
--------------------------------------------------------------------------------
/docs-source/static/troubleshooting.rst:
--------------------------------------------------------------------------------
1 | ===================================
2 | Lacework Python SDK Troubleshooting
3 | ===================================
4 |
5 | This SDK uses standard python logging facilities. To turn on all of the debug information
6 | use the following:
7 |
8 | .. code-block::
9 | :caption: Turn on Debugging
10 |
11 | logging.basicConfig()
12 | logging.getLogger().setLevel(logging.DEBUG)
13 | requests_log = logging.getLogger("requests.packages.urllib3")
14 | requests_log.setLevel(logging.DEBUG)
15 | requests_log.propagate = True
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 | .gitignore
3 | !README.md
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # The Lacework SDK docs are hosted on Github Pages [here](https://lacework.github.io/python-sdk/)
--------------------------------------------------------------------------------
/examples/example_active_container_vulnerabilities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from datetime import datetime, timedelta, timezone
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Build start/end times
22 | current_time = datetime.now(timezone.utc)
23 | start_time = current_time - timedelta(days=1)
24 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
25 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%S%z")
26 |
27 | # Entities API
28 |
29 | # Get active image IDs
30 | active_containers = lacework_client.entities.containers.search(
31 | json={
32 | "timefilter":
33 | {"starTime": start_time,
34 | "endTime": end_time},
35 | "returns": [
36 | "imageId"
37 | ]
38 | }
39 | )
40 |
41 | image_ids = set()
42 | for page in active_containers:
43 | for item in page["data"]:
44 | image_ids.add(item["imageId"])
45 |
46 | # Vulnerabilities API
47 |
48 | active_container_vulns = lacework_client.vulnerabilities.containers.search(
49 | json={
50 | "timefilter":
51 | {
52 | "starTime": start_time,
53 | "endTime": end_time
54 | },
55 | "filters": [
56 | {
57 | "field": "imageId",
58 | "expression": "in",
59 | "values": list(image_ids)
60 | },
61 | {
62 | "field": "severity",
63 | "expression": "in",
64 | "values": [
65 | "Critical",
66 | "High"
67 | ]
68 | },
69 | {
70 | "field": "status",
71 | "expression": "eq",
72 | "value": "VULNERABLE"
73 | },
74 | {
75 | "field": "fixInfo.fix_available",
76 | "expression": "eq",
77 | "value": 1
78 | }
79 | ]
80 | }
81 | )
82 |
83 | for page in active_container_vulns:
84 | # Do something way more interesting with the fixable Critical and High sev
85 | # vulnerabilities for containers that were active in the past 24 hours here...
86 | print(page["paging"]["totalRows"])
87 | exit()
88 |
--------------------------------------------------------------------------------
/examples/example_alert_channels.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from dotenv import load_dotenv
9 | from laceworksdk import LaceworkClient
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 | load_dotenv()
14 |
15 | if __name__ == "__main__":
16 |
17 | # Instantiate a LaceworkClient instance
18 | lacework_client = LaceworkClient()
19 |
20 | # Alert Channels API
21 |
22 | # Get Alert Channels
23 | lacework_client.alert_channels.get()
24 |
25 | # Search Alert Channels
26 | lacework_client.alert_channels.search(json={
27 | "filters": [
28 | {
29 | "expression": "eq",
30 | "field": "type",
31 | "value": "SlackChannel"
32 | }
33 | ],
34 | "returns": [
35 | "intgGuid"
36 | ]
37 | })
38 |
--------------------------------------------------------------------------------
/examples/example_alerts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 | import random
8 |
9 | from datetime import datetime, timedelta, timezone
10 | from dotenv import load_dotenv
11 | from laceworksdk import LaceworkClient
12 |
13 | logging.basicConfig(level=logging.DEBUG)
14 |
15 | load_dotenv()
16 |
17 | if __name__ == "__main__":
18 |
19 | # Instantiate a LaceworkClient instance
20 | lacework_client = LaceworkClient()
21 |
22 | # Build start/end times
23 | current_time = datetime.now(timezone.utc)
24 | start_time = current_time - timedelta(days=1)
25 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
26 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%S%z")
27 |
28 | # Alerts API
29 |
30 | # Get alerts for specified time range
31 | alerts = lacework_client.alerts.get(start_time=start_time, end_time=end_time)
32 |
33 | # Get alert details for specified ID
34 | alert_details = lacework_client.alerts.get_details(random.choice(alerts["data"])["alertId"], "Details")
35 |
--------------------------------------------------------------------------------
/examples/example_audit_logs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from datetime import datetime, timedelta, timezone
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Build start/end times
22 | current_time = datetime.now(timezone.utc)
23 | start_time = current_time - timedelta(days=1)
24 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
25 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
26 |
27 | # Audit Logs API
28 |
29 | # Get Audit Logs
30 | default_scope_logs = lacework_client.audit_logs.get()
31 |
32 | # Get Audit Logs by date range
33 | time_scope_logs = lacework_client.audit_logs.get(start_time=start_time, end_time=end_time)
34 |
35 | # Search Audit Logs
36 | username_scope_logs = lacework_client.audit_logs.search(json={
37 | "timeFilter": {
38 | "startTime": start_time,
39 | "endTime": end_time
40 | },
41 | "filters": [
42 | {
43 | "expression": "rlike",
44 | "field": "userName",
45 | "value": "lacework.net"
46 | }
47 | ],
48 | "returns": [
49 | "accountName",
50 | "userAction",
51 | "userName"
52 | ]
53 | })
54 |
--------------------------------------------------------------------------------
/examples/example_cloud_accounts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 | import random
8 |
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Cloud Accounts API
22 |
23 | # Get all Cloud Accounts
24 | integrations = lacework_client.cloud_accounts.get()
25 |
26 | cloud_account_guid = random.choice(integrations["data"])["intgGuid"]
27 | print(cloud_account_guid)
28 |
29 | # Get Cloud Account by ID
30 | integration_by_id = lacework_client.cloud_accounts.get_by_guid(cloud_account_guid)
31 | print(integration_by_id)
32 |
--------------------------------------------------------------------------------
/examples/example_cloud_activities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from datetime import datetime, timedelta, timezone
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Build start/end times
22 | current_time = datetime.now(timezone.utc)
23 | start_time = current_time - timedelta(days=7)
24 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
25 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
26 |
27 | # Cloud Activities API
28 |
29 | # Get Cloud Activities
30 | cloud_activities_default_scope = lacework_client.cloud_activities.get()
31 |
32 | # Get Cloud Activities by date range
33 | cloud_activities_time_scope = lacework_client.cloud_activities.get(start_time=start_time, end_time=end_time)
34 |
35 | # Search Cloud Activities
36 | cloud_activities_field_scope = lacework_client.cloud_activities.search(json={
37 | "timeFilter": {
38 | "startTime": start_time,
39 | "endTime": end_time
40 | },
41 | "filters": [
42 | {
43 | "expression": "eq",
44 | "field": "eventModel",
45 | "value": "CloudTrailCep"
46 | }
47 | ],
48 | "returns": [
49 | "eventType",
50 | "eventActor"
51 | ]
52 | })
53 |
--------------------------------------------------------------------------------
/examples/example_inventory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from dotenv import load_dotenv
9 | from laceworksdk import LaceworkClient
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 | load_dotenv()
14 |
15 | if __name__ == "__main__":
16 |
17 | # Instantiate a LaceworkClient instance
18 | lacework_client = LaceworkClient()
19 |
20 | # Inventory API
21 |
22 | # Scan each CSP (cloud service provider)
23 |
24 | for csp in ["AWS", "GCP", "Azure"]:
25 | inventory_scan = lacework_client.inventory.scan(csp=csp)
26 |
27 | inventory_scan_status = lacework_client.inventory.status(csp=csp)
28 |
--------------------------------------------------------------------------------
/examples/example_query_policy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 | import random
8 | import string
9 |
10 | from dotenv import load_dotenv
11 | from laceworksdk import LaceworkClient
12 |
13 | logging.basicConfig(level=logging.DEBUG)
14 |
15 | load_dotenv()
16 |
17 | RANDOM_TEXT = "".join(random.choices(string.ascii_uppercase, k=4))
18 | QUERY_ID = f"Custom_Query_{RANDOM_TEXT}"
19 | POLICY_TITLE = f"Custom_Policy_{RANDOM_TEXT}"
20 |
21 | if __name__ == "__main__":
22 |
23 | # Instantiate a LaceworkClient instance
24 | lacework_client = LaceworkClient()
25 |
26 | # Queries/Policies API
27 |
28 | # Create a Query
29 | query_response = lacework_client.queries.create(
30 | evaluator_id="Cloudtrail",
31 | query_id=QUERY_ID,
32 | query_text="""{
33 | source {CloudTrailRawEvents e}
34 | filter {EVENT_SOURCE = 'iam.amazonaws.com' AND
35 | EVENT:userIdentity.name::String NOT LIKE 'Terraform-Service-Acct'}
36 | return distinct {EVENT_NAME, EVENT}
37 | }
38 | """
39 | )
40 |
41 | # Create a Policy
42 | lacework_client.policies.create(
43 | policy_type="Violation",
44 | query_id=query_response["data"]["queryId"],
45 | enabled=True,
46 | title=POLICY_TITLE,
47 | description=f"{POLICY_TITLE}_Description",
48 | remediation="Policy remediation",
49 | severity="high",
50 | alert_enabled=True,
51 | alert_profile="LW_CloudTrail_Alerts",
52 | evaluator_id=query_response["data"]["evaluatorId"]
53 | )
54 |
--------------------------------------------------------------------------------
/examples/example_reports.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from dotenv import load_dotenv
9 | from laceworksdk import LaceworkClient
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 | load_dotenv()
14 |
15 | if __name__ == "__main__":
16 |
17 | from laceworksdk import LaceworkClient
18 |
19 | lw = LaceworkClient(profile="default")
20 |
21 | # Get a list of accounts
22 | accounts = lw.cloud_accounts.get()['data']
23 |
24 | # List comprehension to filter out disabled or misconfigured integrations
25 | # as well as only select for "config" type integrations
26 | config_accounts = [account for account in accounts if
27 | ("Cfg" in account['type'] and account['enabled'] == 1 and account['state']['ok'] is True)]
28 |
29 | # Loop through what's left and find the first AWS integration
30 | for config_account in config_accounts:
31 | if config_account['type'] == 'AwsCfg':
32 | # Parse the AWS account ID from the account details
33 | arn_elements = config_account['data']['crossAccountCredentials']['roleArn'].split(':')
34 | primary_query_id = arn_elements[4]
35 | break
36 |
37 | # Leverage the retrieved account ID to pull a CIS 1.4 report for that account
38 | # in html format
39 | response = lw.reports.get(primary_query_id=primary_query_id,
40 | format="html",
41 | type="COMPLIANCE",
42 | report_type="AWS_CIS_14")
43 |
--------------------------------------------------------------------------------
/examples/example_schemas.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from dotenv import load_dotenv
9 | from laceworksdk import LaceworkClient
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 | load_dotenv()
14 |
15 | if __name__ == "__main__":
16 |
17 | # Instantiate a LaceworkClient instance
18 | lacework_client = LaceworkClient()
19 |
20 | # Schemas API
21 |
22 | # Get Schemas
23 | lacework_client.schemas.get()
24 |
--------------------------------------------------------------------------------
/examples/example_syscall_query_policy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class with Syscall data sources.
4 | Note: As of this commit, the Lacework Syscall agent is not GA. Please contact your Lacework rep for more information
5 | """
6 |
7 | import logging
8 | import random
9 | import string
10 |
11 | from dotenv import load_dotenv
12 | from laceworksdk import LaceworkClient
13 |
14 | logging.basicConfig(level=logging.DEBUG)
15 |
16 | load_dotenv()
17 |
18 | RANDOM_TEXT = "".join(random.choices(string.ascii_uppercase, k=4))
19 | QUERY_ID = f"Custom_Syscall_Query_{RANDOM_TEXT}"
20 | POLICY_TITLE = f"Custom_Syscall_Policy_{RANDOM_TEXT}"
21 |
22 | if __name__ == "__main__":
23 |
24 | # Instantiate a LaceworkClient instance
25 | lacework_client = LaceworkClient()
26 |
27 | # Queries/Policies API
28 |
29 | # Create a Query
30 | query_response = lacework_client.queries.create(
31 | query_id=QUERY_ID,
32 | query_text="""{
33 | source {
34 | LW_HA_SYSCALLS_FILE
35 | }
36 | filter {
37 | TARGET_OP like any('create','modify') AND TARGET_PATH like any('%/.ssh/authorized_keys','%/ssh/sshd_config')
38 | }
39 | return distinct {
40 | RECORD_CREATED_TIME,
41 | MID,
42 | TARGET_OP,
43 | TARGET_PATH
44 | }
45 | }
46 | """
47 | )
48 |
49 | # Create a Policy, uncomment alternate alert_profiles as required
50 | lacework_client.policies.create(
51 | policy_type="Violation",
52 | query_id=query_response["data"]["queryId"],
53 | enabled=True,
54 | title=POLICY_TITLE,
55 | description="Description here..",
56 | remediation="Policy remediation here..",
57 | severity="high",
58 | alert_enabled=True,
59 | alert_profile="LW_HA_SYSCALLS_FILE_DEFAULT_PROFILE.Violation"
60 | )
61 |
--------------------------------------------------------------------------------
/examples/example_team_users.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from dotenv import load_dotenv
9 | from laceworksdk import LaceworkClient
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 | load_dotenv()
14 |
15 |
16 | def standard_user_example(client: LaceworkClient):
17 | """
18 | Example of create/update/delete and group management for standard user
19 | """
20 |
21 | # Create user
22 | data = client.team_users.create("test user", "noreply@lacework.com", "test-company")
23 | guid = data["data"]["userGuid"]
24 | logging.debug(f'user guid created:\n{guid}')
25 |
26 | # Get one user
27 | client.team_users.get(guid)
28 |
29 | # Update user
30 | client.team_users.update(guid, user_enabled=0)
31 |
32 | # Add user to group
33 | client.user_groups.add_users("LACEWORK_USER_GROUP_POWER_USER", [guid])
34 |
35 | # Remove user from group
36 | client.user_groups.remove_users("LACEWORK_USER_GROUP_POWER_USER", [guid])
37 |
38 | # Delete user
39 | client.team_users.delete(guid)
40 |
41 | def service_user_example(client: LaceworkClient):
42 | """
43 | Example of create/update/delete and group management for service user
44 | """
45 |
46 | # Create user
47 | data = client.team_users.create("test service user", description="test service user", type="ServiceUser")
48 | guid = data["data"]["userGuid"]
49 | logging.debug(f'user guid created:\n{guid}')
50 |
51 | # Get one user
52 | client.team_users.get(guid)
53 |
54 | # Update user
55 | client.team_users.update(guid, user_enabled=0)
56 |
57 | # Add user to group
58 | client.user_groups.add_users("LACEWORK_USER_GROUP_POWER_USER", [guid])
59 |
60 | # Remove user from group
61 | client.user_groups.remove_users("LACEWORK_USER_GROUP_POWER_USER", [guid])
62 |
63 | # Delete user
64 | client.team_users.delete(guid)
65 |
66 |
67 |
68 | if __name__ == "__main__":
69 | # Instantiate a LaceworkClient instance
70 | lacework_client = LaceworkClient()
71 |
72 | # TeamUsers API
73 |
74 | # Get users
75 | lacework_client.team_users.get()
76 |
77 | standard_user_example(lacework_client)
78 | service_user_example(lacework_client)
79 |
80 |
--------------------------------------------------------------------------------
/examples/example_tokens.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 | import random
8 |
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Agent Access Token API
22 |
23 | # Get all Agent Access Tokens
24 | agent_api_tokens = lacework_client.agent_access_tokens.get()
25 |
26 | # Get specified Agent Access Token
27 | api_token = lacework_client.agent_access_tokens.get_by_id(random.choice(agent_api_tokens["data"])["accessToken"])
28 |
--------------------------------------------------------------------------------
/examples/example_vulnerabilities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Example script showing how to use the LaceworkClient class.
4 | """
5 |
6 | import logging
7 |
8 | from datetime import datetime, timedelta, timezone
9 | from dotenv import load_dotenv
10 | from laceworksdk import LaceworkClient
11 |
12 | logging.basicConfig(level=logging.DEBUG)
13 |
14 | load_dotenv()
15 |
16 | if __name__ == "__main__":
17 |
18 | # Instantiate a LaceworkClient instance
19 | lacework_client = LaceworkClient()
20 |
21 | # Build start/end times
22 | current_time = datetime.now(timezone.utc)
23 | start_time = current_time - timedelta(days=6)
24 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S%z")
25 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%S%z")
26 |
27 | # Vulnerability API
28 |
29 | # Host
30 |
31 | # This yields a generator
32 | host_vulns = lacework_client.vulnerabilities.hosts.search(json={
33 | "timeFilter": {
34 | "startTime": start_time,
35 | "endTime": end_time
36 | }
37 | })
38 |
39 | # get the first page of data from the generator using "next" and print it
40 | print(next(host_vulns)['data'])
41 | # Containers
42 |
43 | container_vulns = lacework_client.vulnerabilities.containers.search({
44 | "timeFilter": {
45 | "startTime": start_time,
46 | "endTime": end_time
47 | }
48 | })
49 |
50 | # iterate through the generator but let's stop at the first page so we don't have to see all the container vulns
51 | for page in container_vulns:
52 | print(page['data'])
53 | # we wouldn't normally break here but for this example there's no reason to retrieve all the pages
54 | break
55 |
--------------------------------------------------------------------------------
/jupyter/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include laceworkjupyter/features/*.yaml
2 |
--------------------------------------------------------------------------------
/jupyter/devtools/deploy_to_container.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Simple Bash script to deploy the current developer branch to the lacebook
3 | # container.
4 | #
5 | # This script is used by developers to push locally made changes to an already
6 | # running Lacebook container.
7 | #
8 | # This script needs to be run in the root folder, eg:
9 | # sh jupyter/devtools/deploy_to_container.sh
10 | #
11 | # Then inside the container select "reset runtime" and the new changes
12 | # should be applied.
13 |
14 | if [ ! -d "jupyter" ];
15 | then
16 | echo "Unable to run, this needs to run in the root folder.";
17 | exit 1;
18 | fi
19 |
20 |
21 | if [ ! -f "jupyter/devtools/deploy_to_container.sh" ];
22 | then
23 | echo "Unable to run, this needs to run in the root folder.";
24 | exit 1;
25 | fi
26 |
27 | echo "Deleting old remnants of developer updates."
28 | docker exec -u root lacebook bash -c "rm -rf /home/lacework/jup*"
29 | echo "Adding the current code to the container"
30 | docker cp jupyter lacebook:/home/lacework/jup
31 |
32 | echo "Updating the source"
33 | docker exec lacebook bash -c "cd /home/lacework && source lacenv/bin/activate && tar cfvz jup.tgz jup && pip install --upgrade jup.tgz"
34 |
35 | echo "If there wasn't any error, you can now restart the runtime inside the container."
36 |
--------------------------------------------------------------------------------
/jupyter/docker/docker-build.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | lacebook:
4 | container_name: lacebook
5 | build:
6 | context: ../
7 | dockerfile: ./docker/docker_build/Dockerfile
8 | ports:
9 | - 127.0.0.1:8899:8899
10 | restart: on-failure
11 | volumes:
12 | - ../../:/usr/local/src/python-sdk/:ro
13 | - $HOME/.lacework.toml:/home/lacework/.lacework.toml
14 | - /tmp/:/usr/local/src/lacedata/
15 |
--------------------------------------------------------------------------------
/jupyter/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | lacebook:
4 | container_name: lacebook
5 | image: docker.io/lacework/lacebook:latest
6 | ports:
7 | - 127.0.0.1:8899:8899
8 | restart: on-failure
9 | volumes:
10 | - $HOME/.lacework.toml:/home/lacework/.lacework.toml
11 | - /tmp/:/usr/local/src/lacedata/
12 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/00-import.py:
--------------------------------------------------------------------------------
1 | """This is an import file that runs on every startup of the Jupyter runtime."""
2 | # flake8: noqa
3 |
4 | import altair as alt
5 | import pandas as pd
6 | import numpy as np
7 |
8 | import laceworkjupyter
9 | from laceworkjupyter import utils
10 |
11 | # Keeping for legacy reasons.
12 | from laceworkjupyter.helper import LaceworkJupyterClient as LaceworkJupyterHelper
13 |
14 | import snowflake.connector
15 |
16 | # Import forensic tools designed for notebooks.
17 | # TODO (kiddi): Re-enable once TS API has been fiex, see #2388 on Timesketch.
18 | #from picatrix import notebook_init
19 | import ds4n6_lib as ds
20 |
21 | # Add in the accessors to pandas.
22 | from laceworkjupyter import accessors
23 |
24 | # Enable the Picatrix helpers.
25 | # TODO (kiddi): Re-enable once TS is fixed, see above.
26 | #notebook_init.init()
27 |
28 | # Enable the LW object.
29 | lw = laceworkjupyter.LaceworkHelper()
30 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8-slim
2 |
3 | # Create folders and fix permissions.
4 | RUN groupadd --gid 1000 lacegroup && \
5 | useradd lacework --uid 1000 --gid 1000 -d /home/lacework -m && \
6 | mkdir -p /usr/local/src/lacedata/ && \
7 | chmod 777 /usr/local/src/lacedata/ && \
8 | echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \
9 | apt-get update && apt-get install -y --no-install-recommends git dnsutils whois
10 |
11 | USER lacework
12 | WORKDIR /home/lacework
13 | ENV VIRTUAL_ENV=/home/lacework/lacenv
14 |
15 | RUN python3 -m venv $VIRTUAL_ENV && \
16 | mkdir -p .ipython/profile_default/startup/ && \
17 | mkdir -p /home/lacework/.jupyter && \
18 | mkdir -p /home/lacework/.local/share/jupyter/nbextensions/snippets/ && \
19 | mkdir -p /home/lacework/.jupyter/custom && \
20 | cd /home/lacework && git clone https://github.com/lacework/python-sdk.git && mv python-sdk code
21 |
22 | ENV PATH="$VIRTUAL_ENV/bin:$PATH"
23 | ENV JUPYTER_PORT=8899
24 |
25 | COPY --chown=1000:1000 docker/docker_build/00-import.py /home/lacework/.ipython/profile_default/startup/00-import.py
26 | COPY --chown=1000:1000 docker/docker_build/jupyter_notebook_config.py /home/lacework/.jupyter/jupyter_notebook_config.py
27 | COPY --chown=1000:1000 docker/docker_build/logo.png /home/lacework/.jupyter/custom/logo.png
28 | COPY --chown=1000:1000 docker/docker_build/custom.css /home/lacework/.jupyter/custom/custom.css
29 | COPY --chown=1000:1000 docker/docker_build/lacework /home/lacework/lacenv/share/jupyter/nbextensions/lacework
30 |
31 |
32 | RUN pip install --upgrade pip setuptools wheel && \
33 | pip install --upgrade ipywidgets jupyter_contrib_nbextensions jupyter_http_over_ws ipydatetime tabulate && \
34 | pip install --upgrade scikit-learn matplotlib python-evtx Evtx timesketch_import_client "snowflake-connector-python[secure-local-storage,pandas]" && \
35 | pip install --upgrade ipyaggrid keras nbformat numpy pandas pyparsing qgrid ruamel.yaml sklearn mitreattack-python && \
36 | pip install --upgrade tensorflow tqdm traitlets xmltodict ds4n6-lib picatrix timesketch_api_client openpyxl && \
37 | cd /home/lacework/code && pip install -e . && \
38 | cd /home/lacework/code/jupyter && pip install -e . && \
39 | jupyter serverextension enable --py jupyter_http_over_ws && \
40 | jupyter nbextension enable --py widgetsnbextension --sys-prefix && \
41 | jupyter contrib nbextension install --user && \
42 | jupyter nbextensions_configurator enable --user && \
43 | #jupyter nbextension enable --py --user ipyaggrid && \
44 | jupyter nbextension enable snippets/main && \
45 | jupyter nbextension enable lacework/main && \
46 | jupyter nbextension install --user --py ipydatetime && \
47 | jupyter nbextension enable --user --py ipydatetime
48 |
49 | COPY --chown=1000:1000 docker/docker_build/snippets.json /home/lacework/.local/share/jupyter/nbextensions/snippets/snippets.json
50 |
51 | WORKDIR /usr/local/src/lacedata/
52 | EXPOSE 8899
53 |
54 | # Run jupyter.
55 | ENTRYPOINT ["jupyter", "notebook"]
56 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/custom.css:
--------------------------------------------------------------------------------
1 | #ipython_notebook img{
2 | display:block;
3 | background: url("logo.png") no-repeat;
4 | background-size: contain;
5 | width: 83px;
6 | height: 35px;
7 | padding-left: 90px;
8 | padding-bottom: 50px;
9 | -moz-box-sizing: border-box;
10 | box-sizing: border-box;
11 | }
12 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/jupyter_notebook_config.py:
--------------------------------------------------------------------------------
1 | # Configuration file for jupyter-notebook.
2 | # flake8: noqa
3 |
4 | ## Use a regular expression for the Access-Control-Allow-Origin header
5 | #
6 | # Requests from an origin matching the expression will get replies with:
7 | #
8 | # Access-Control-Allow-Origin: origin
9 | #
10 | # where `origin` is the origin of the request.
11 | #
12 | # Ignored if allow_origin is set.
13 | c.NotebookApp.allow_origin_pat = 'https://colab.[a-z]+.google.com'
14 |
15 | ## The IP address the notebook server will listen on.
16 | c.NotebookApp.ip = '*'
17 |
18 | ## The directory to use for notebooks and kernels.
19 | # Uncomment this if you want the notebook to start immediately in this folder.
20 | #c.NotebookApp.notebook_dir = 'data/'
21 |
22 | ## Whether to open in a browser after starting. The specific browser used is
23 | # platform dependent and determined by the python standard library `webbrowser`
24 | # module, unless it is overridden using the --browser (NotebookApp.browser)
25 | # configuration option.
26 | c.NotebookApp.open_browser = False
27 |
28 | ## Hashed password to use for web authentication.
29 | #
30 | # To generate, type in a python/IPython shell:
31 | #
32 | # from notebook.auth import passwd; passwd()
33 | #
34 | # The string should be of the form type:salt:hashed-password.
35 | # If this is enabled the password "lacework" can be used.
36 | #c.NotebookApp.password = 'argon2:$argon2id$v=19$m=10240,t=10,p=8$Q2sRv8dVZ8WBSmGNcTMuKg$VtFU0bwX81Ou+OaDWQgloA'
37 | # Right now the token is set to lacework, a plain text password.
38 | c.NotebookApp.token = 'lacework'
39 |
40 | ## The port the notebook server will listen on.
41 | c.NotebookApp.port = 8899
42 |
43 | ## The number of additional ports to try if the specified port is not available.
44 | c.NotebookApp.port_retries = 0
45 |
46 | ## The base name used when creating untitled notebooks.
47 | c.ContentsManager.untitled_notebook = 'NewLacebook'
48 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/lacework/README.md:
--------------------------------------------------------------------------------
1 | Lacework
2 | =========
3 |
4 | This is a simple nbextension that just adds few default cells to each notebook
5 | that is created within the notebook container.
6 |
7 | This extension can be easily adapted and changed to better suit users of the
8 | notebook, this is simply meant as a single example of how this can be
9 | achieved.
10 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/lacework/lacework.yaml:
--------------------------------------------------------------------------------
1 | Type: IPython Notebook Extension
2 | Compatibility: 4.x, 5.x, 6.x
3 | Name: Lacework
4 | Main: main.js
5 | Link: README.md
6 | Description: Add few default cells to a new notebook.
7 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/lacework/main.js:
--------------------------------------------------------------------------------
1 | // Code cell snippets
2 |
3 | define([
4 | 'base/js/namespace',
5 | ], function(
6 | Jupyter,
7 | ) {
8 | "use strict";
9 |
10 | // will be called when the nbextension is loaded
11 | function load_extension() {
12 | var ncells = Jupyter.notebook.ncells();
13 | if (ncells > 1) {
14 | return true;
15 | }
16 |
17 | var new_cell = Jupyter.notebook.insert_cell_above('markdown', 0);
18 | new_cell.set_text('# Lacework Notebook\nChange this text to reflect what this notebook attempts to accomplish.\n**Remember to rename the notebook itself as well**.');
19 | new_cell.render();
20 | new_cell.focus_cell();
21 |
22 | var import_cell = Jupyter.notebook.insert_cell_below('code');
23 | import_cell.set_text('client = lw.get_client()');
24 |
25 | var new_cell = Jupyter.notebook.insert_cell_below('markdown');
26 | new_cell.set_text('## Connecting to Lacework\nBasic imports have already been completed, we now need to connect to the Lacework API using the Jupyter helper.\nExecute the cell below by pressing the play button or using "shift + enter"\n\nAlll Lacework SDK actions happen on either the lw object or the client, which can be gathered using the lw.get_client() function.');
27 | new_cell.render();
28 | import_cell.focus_cell();
29 | };
30 |
31 | // return public methods
32 | return {
33 | load_ipython_extension : load_extension
34 | };
35 | });
36 |
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lacework/python-sdk/3b74b39cc26e5a8effffd8c09b02a17a04307a1f/jupyter/docker/docker_build/logo.png
--------------------------------------------------------------------------------
/jupyter/docker/docker_build/snippets.json:
--------------------------------------------------------------------------------
1 | {
2 | "snippets": [
3 | {
4 | "name": "Get a new client",
5 | "code" : [
6 | "client = lw.get_client()"
7 | ]
8 | },
9 | {
10 | "name": "Generate a Date Picker for start/end times",
11 | "code" : [
12 | "import ipydatetime",
13 | "import ipywidgets as widgets",
14 | "import pytz",
15 | "",
16 | "start_time_form = ipydatetime.DatetimePicker(tzinfo=pytz.utc)",
17 | "end_time_form = ipydatetime.DatetimePicker(tzinfo=pytz.utc)",
18 | "start_label = widgets.Label(",
19 | " value='Start Time:',",
20 | ")",
21 | "end_label = widgets.Label(",
22 | " value='End Time:')",
23 | "button = widgets.Button(description='Set Time')",
24 | "",
25 | "date_grid = widgets.TwoByTwoLayout(",
26 | " top_left=start_label,",
27 | " top_right=start_time_form,",
28 | " bottom_left=end_label,",
29 | " bottom_right=end_time_form,",
30 | " justify_items='right',",
31 | " width='300px',",
32 | " align_items='center',",
33 | " grid_gap='10px')",
34 | "",
35 | "display(date_grid)",
36 | "display(button)",
37 | "",
38 | "start_time = ''",
39 | "end_time = ''",
40 | "",
41 | "def _click_function(_):",
42 | " ip = get_ipython()",
43 | " ip.push({",
44 | " 'start_time': start_time_form.value.strftime('%Y-%m-%dT%H:%M:%S'),",
45 | " 'end_time': end_time_form.value.strftime('%Y-%m-%dT%H:%M:%S')",
46 | " })",
47 | " display(Markdown(",
48 | " 'Results are stored in **start_time** and **end_time**'))",
49 | " display(Markdown(",
50 | " 'You can now use these two variables in functions.'))",
51 | "",
52 | "button.on_click(_click_function)"
53 | ]
54 | },
55 | {
56 | "name" : "Use date offsets to get start and end times.",
57 | "code" : [
58 | "start_time, end_time = utils.parse_date_offset('LAST 7 days')"
59 | ]
60 | }
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | Package configuration.
4 | '''
5 |
6 | # Package Constants
7 | DEFAULT_BASE_DOMAIN = 'lacework.net'
8 |
9 | # Config file paths.
10 | DEFAULT_REL_CONFIG_PATH = '.lacework.toml'
11 |
12 | # Specific field configuration.
13 | SEVERITY_DICT = {
14 | '1': 'Critical',
15 | '2': 'High',
16 | '3': 'Medium',
17 | '4': 'Low',
18 | '5': 'Info'
19 | }
20 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/decorators.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import types
3 |
4 | import pandas as pd
5 |
6 | from . import config
7 |
8 |
9 | def dataframe_decorator(function):
10 | """
11 | A decorator used to convert Lacework JSON API output into a dataframe.
12 | """
13 | def _get_frame_from_dict(data):
14 | if not isinstance(data, dict):
15 | return
16 |
17 | data_items = data.get('data', [])
18 | if isinstance(data_items, dict):
19 | data_items = [data_items]
20 |
21 | df = pd.DataFrame(data_items)
22 | if 'SEVERITY' in df:
23 | df['SEVERITY'] = df.SEVERITY.apply(
24 | lambda x: config.SEVERITY_DICT.get(x, x))
25 | return df
26 |
27 | @functools.wraps(function)
28 | def get_output(*args, **kwargs):
29 | data = function(*args, **kwargs)
30 |
31 | if isinstance(data, dict):
32 | return _get_frame_from_dict(data)
33 |
34 | elif isinstance(data, (types.GeneratorType, list, map, filter)):
35 | frames = [_get_frame_from_dict(x) for x in data]
36 | return pd.concat(frames)
37 |
38 | return data
39 |
40 | return get_output
41 |
42 |
43 | def plugin_decorator(function, output_plugin):
44 | """
45 | A decorator used to use a plugin to convert Lacework JSON API output.
46 | """
47 | @functools.wraps(function)
48 | def get_output(*args, **kwargs):
49 | data = function(*args, **kwargs)
50 | return output_plugin(data)
51 |
52 | return get_output
53 |
54 |
55 | def feature_decorator(function, ctx=None):
56 | """
57 | A decorator that adds a context to a function call.
58 | """
59 | @functools.wraps(function)
60 | def wrapper(*args, **kwargs):
61 | if ctx:
62 | kwargs['ctx'] = ctx
63 | return function(*args, **kwargs)
64 |
65 | return wrapper
66 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/__init__.py:
--------------------------------------------------------------------------------
1 | """A simple loading of helper functions."""
2 | # flake8: noqa
3 |
4 | from . import cache
5 | from . import client
6 | from . import date
7 | from . import helper
8 | from . import hunt
9 | from . import mitre
10 | from . import policies
11 | from . import query
12 | from . import query_builder
13 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/cache.py:
--------------------------------------------------------------------------------
1 | """
2 | Simple feature to get values from cache.
3 | """
4 |
5 | from laceworkjupyter import manager
6 |
7 |
8 | @manager.register_feature
9 | def get_from_cache(key, default_value=None, ctx=None):
10 | """Returns value from the context cache.
11 |
12 | :param str key: String with the key.
13 | :param obj default_value: Optional default value, if the
14 | key is not found inside the cache this value will be
15 | returned, defaults to None.
16 | :param obj ctx: The context object.
17 | :returns: The value in the cache that corresponds to the provided key.
18 | """
19 | if not ctx:
20 | return default_value
21 |
22 | return ctx.get(key, default_value)
23 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/client.py:
--------------------------------------------------------------------------------
1 | """A simple class that contains Lacework API related features."""
2 |
3 | import datetime
4 |
5 | import pandas as pd
6 |
7 | from laceworkjupyter import helper
8 | from laceworkjupyter import manager
9 |
10 |
11 | DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
12 |
13 |
14 | @manager.register_feature
15 | def get_client(
16 | api_key=None, api_secret=None, account=None,
17 | subaccount=None, instance=None, base_domain=None,
18 | profile=None, ctx=None):
19 | """
20 | Returns a Lacework API client for use in a notebook.
21 |
22 | :return: An instance of a LaceworkJupyterClient.
23 | """
24 |
25 | client = helper.LaceworkJupyterClient(
26 | api_key=api_key, api_secret=api_secret, account=account,
27 | subaccount=subaccount, instance=instance, base_domain=base_domain,
28 | profile=profile)
29 |
30 | ctx.set_client(client)
31 | return client
32 |
33 |
34 | @manager.register_feature
35 | def get_events_from_alert(alert_id, client=None, ctx=None):
36 | """
37 | Return a data frame with the attached events from a single alert.
38 |
39 | :param str alert_id: The ID of the alert.
40 | :param client: Optional client object, defaults to using the client
41 | that is stored in the context, or by fetching a default client.
42 | :return: A pandas DataFrame with the evidence associated with the event.
43 | """
44 | if not client:
45 | if ctx:
46 | client = ctx.client
47 | if not client:
48 | client = get_client()
49 |
50 | return client.alerts.get_details(alert_id, scope='Events')
51 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/date.py:
--------------------------------------------------------------------------------
1 | """
2 | A simple class to wrap the date parsing function in the utilities lib.
3 | """
4 | from laceworkjupyter import manager
5 | from laceworkjupyter import utils
6 |
7 |
8 | @manager.register_feature
9 | def parse_date_offset(offset_string, ctx=None):
10 | """
11 | Parse date offset string and return a start and end time.
12 |
13 | :param str offset_string: The offset string describing the time period.
14 | :param obj ctx: The Lacework context object.
15 | :raises ValueError: If not able to convert the string to dates.
16 | :return: A tuple with start and end time as ISO 8601 formatted strings.
17 | """
18 | start_time, end_time = utils.parse_date_offset(offset_string)
19 | ctx.add("start_time", start_time)
20 | ctx.add("end_time", end_time)
21 | return start_time, end_time
22 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/helper.py:
--------------------------------------------------------------------------------
1 | """
2 | File that contains data frame helpers for the Lacework notebook environment.
3 | """
4 |
5 | import json
6 | import logging
7 |
8 | import pandas as pd
9 | import numpy as np
10 |
11 | from laceworkjupyter import manager
12 |
13 |
14 | logger = logging.getLogger("lacework_sdk.jupyter.feature.helper")
15 |
16 |
17 | def extract_json_field(json_obj, item):
18 | """
19 | Extract a field from a JSON struct.
20 |
21 | :param json_obj: Either a JSON string or a dict.
22 | :param str item: The item string, dot delimited.
23 | :return: The extracted field.
24 | """
25 | if isinstance(json_obj, str):
26 | try:
27 | json_obj = json.loads(json_obj)
28 | except json.JSONDecodeError:
29 | logger.error("Unable to decode JSON string: %s", json_obj)
30 | return np.nan
31 |
32 | if not isinstance(json_obj, dict):
33 | logger.error("Unable to extract, not a dict: %s", type(json_obj))
34 | return np.nan
35 |
36 | data = json_obj
37 | data_points = item.split(".")
38 | for index, point in enumerate(data_points):
39 | if not isinstance(data, dict):
40 | if index != (len(data_points) - 1):
41 | logger.error(
42 | "Sub-item %s is not a dict (%s)", point, type(data))
43 | return np.nan
44 |
45 | data = data.get(point)
46 | return data
47 |
48 |
49 | @manager.register_feature
50 | def deep_extract_field(data_frame, column, field_string, ctx=None):
51 | """
52 | Extract a field from a JSON struct inside a DataFrame.
53 |
54 | Usage example:
55 | df['hostname'] = lw.deep_extract_field(
56 | df, 'properties', 'host.hostname')
57 |
58 | :param DataFrame data_frame: The data frame to extract from.
59 | :param str column: The name of the column that contains the JSON struct.
60 | :param str field_string: String that contains the field to extract from,
61 | this is a dot delimited string, eg: key.foo.bar, that will extract
62 | a value from {'key': 'foo': {'bar': 'value'}}.
63 | :param obj ctx: The context object.
64 | :return: A pandas Series with the extracted value.
65 | """
66 | if column not in data_frame:
67 | logger.error("Column does not exist in the dataframe.")
68 | return pd.Series()
69 |
70 | return data_frame[column].apply(
71 | lambda x: extract_json_field(x, field_string))
72 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/join.yaml:
--------------------------------------------------------------------------------
1 | - description: Host Information
2 | from: LW_HE_MACHINES
3 | alias: machine
4 | return_fields:
5 | - HOSTNAME AS MACHINE_HOST
6 | - DOMAIN AS MACHINE_DOMAIN
7 | - OS_DESC AS MACHINE_OS_DESC
8 | - OS AS MACHINE_OS
9 | to:
10 | - LW_HA_DNS_REQUESTS
11 | - LW_HA_FILE_CHANGES
12 | - LW_HA_USER_LOGINS
13 | - LW_HE_CONTAINERS
14 | - LW_HE_FILES
15 | - LW_HE_IMAGES
16 |
17 | - description: User Information
18 | from: LW_HE_USERS
19 | alias: user
20 | return_fields:
21 | - PRIMARY_GROUP_NAME AS USER_PRIMARY_GROUP
22 | - OTHER_GROUP_NAMES AS USER_GROUPS
23 | - HOME_DIR AS USER_HOME_DIR
24 | to:
25 | - LW_HE_PROCESSES
26 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/mitre.yaml:
--------------------------------------------------------------------------------
1 | - alert_fields:
2 | - name: alertType
3 | value: NewUser
4 | type: exact
5 | id: T1136.003
6 |
7 | - alert_fields:
8 | - name: alertType
9 | value: MaliciousFile
10 | type: exact
11 | id: T1204.002
12 |
13 | - alert_fields:
14 | - name: alertType
15 | value: SuspiciousApplicationLaunched
16 | type: exact
17 | - name: keys.src.keys.exe_path
18 | value: python
19 | type: contains
20 | condition: and
21 | id: T1059.006
22 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/policies.py:
--------------------------------------------------------------------------------
1 | """Simple file to list and execute LQL policies."""
2 |
3 | import logging
4 |
5 | import pandas as pd
6 |
7 | from laceworksdk import http_session
8 | from laceworkjupyter import manager
9 | from laceworkjupyter.features import utils
10 |
11 |
12 | logger = logging.getLogger("lacework_sdk.jupyter.feature.policies")
13 |
14 |
15 | @manager.register_feature
16 | def list_available_queries(ctx=None):
17 | """
18 | Returns a DataFrame with the available LQL queries.
19 |
20 | :return: Pandas DataFrame with available LQL quries.
21 | """
22 | return ctx.client.queries.get()
23 |
24 |
25 | @manager.register_feature
26 | def query_stored_lql(query_id, start_time="", end_time="", ctx=None):
27 | """
28 | Returns the results from running a LQL query.
29 |
30 | This is a simple feature that simply exposes the ability of the client
31 | to run stored LQL queries.
32 |
33 | :param str query_id: The ID of the query.
34 | :param str start_time: ISO formatted start time, if not provided defaults
35 | to two days ago.
36 | :param str end_time: ISO formatted end time. If not provided defaults
37 | to current time.
38 | :param obj ctx: The Lacework context object.
39 | :return: Returns a pandas DataFrame with the results of running the query.
40 | """
41 | client = ctx.client
42 |
43 | try:
44 | _ = client.sdk.queries.get_by_id(query_id)
45 | except http_session.ApiError as err:
46 | logger.error("Query ID not found: {}".format(err))
47 | return pd.DataFrame()
48 |
49 | default_start_time, default_end_time = utils.get_start_and_end_time(ctx)
50 | if not start_time:
51 | start_time = default_start_time
52 |
53 | if not end_time:
54 | end_time = default_end_time
55 |
56 | arguments = {
57 | "StartTimeRange": start_time,
58 | "EndTimeRange": end_time
59 | }
60 |
61 | return client.queries.execute_by_id(
62 | query_id=query_id, arguments=arguments)
63 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/query.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | queries:
3 | - name: "api_source_ip"
4 | evaluator: "Cloudtrail"
5 | params:
6 | - name: "ip_address"
7 | type: "str"
8 | query: |-
9 | APICheckIpSource {
10 | SOURCE {
11 | CloudTrailRawEvents
12 | }
13 | FILTER {
14 | EVENT:sourceIPAddress = '<>'
15 | AND ERROR_CODE IS NULL
16 | }
17 | RETURN DISTINCT {
18 | INSERT_ID,
19 | INSERT_TIME,
20 | EVENT_TIME,
21 | EVENT
22 | }
23 | }
24 |
25 | - name: "dns_to_ip"
26 | params:
27 | - name: "ip_address"
28 | type: "str"
29 | query: |-
30 | Test_DNS_Resolution {
31 | SOURCE {
32 | LW_HA_DNS_REQUESTS
33 | }
34 | FILTER {
35 | HOST_IP_ADDR = '<>'
36 | }
37 | RETURN DISTINCT {
38 | MID,
39 | SRV_IP_ADDR,
40 | HOSTNAME,
41 | HOST_IP_ADDR,
42 | TTL,
43 | PKTLEN
44 | }
45 | }
46 |
47 | - name: "dns_to_hostname"
48 | params:
49 | - name: "hostname"
50 | type: "str"
51 | query: |-
52 | Test_DNS_Resolution {
53 | SOURCE {
54 | LW_HA_DNS_REQUESTS
55 | }
56 | FILTER {
57 | HOSTNAME LIKE '%<>%'
58 | }
59 | RETURN DISTINCT {
60 | MID,
61 | SRV_IP_ADDR,
62 | HOSTNAME,
63 | HOST_IP_ADDR,
64 | TTL,
65 | PKTLEN
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/features/tables.yaml:
--------------------------------------------------------------------------------
1 | - display_name: Agent DNS Requests
2 | name: LW_HA_DNS_REQUESTS
3 | return_fields:
4 | - RECORD_CREATED_TIME
5 | - MID
6 | - SRV_IP_ADDR
7 | - HOSTNAME
8 | - HOST_IP_ADDR
9 | - TTL
10 | - PKTLEN
11 |
12 | - display_name: AWS CloudTrail Logs
13 | evaluator_id: Cloudtrail
14 | name: CloudTrailRawEvents
15 | return_fields:
16 | - INSERT_ID
17 | - INSERT_TIME
18 | - EVENT_TIME
19 | - EVENT
20 |
21 | - display_name: AWS IAM List Roles
22 | name: LW_CFG_AWS_IAM_LIST_ROLES
23 | return_fields:
24 | - ARN
25 | - API_KEY
26 | - ACCOUNT_ID
27 | - ACCOUNT_ALIAS
28 | - RESOURCE_TYPE
29 | - RESOURCE_ID
30 | - RESOURCE_REGION
31 | - RESOURCE_CONFIG
32 | - RESOURCE_TAGS
33 | - STATUS
34 | - KEYS
35 | - PROPS
36 |
37 | - display_name: AWS RDS DB Instances
38 | name: LW_CFG_AWS_RDS_DESCRIBE_DB_INSTANCES
39 | return_fields:
40 | - ARN
41 | - API_KEY
42 | - SERVICE
43 | - ACCOUNT_ID
44 | - ACCOUNT_ALIAS
45 | - RESOURCE_TYPE
46 | - RESOURCE_ID
47 | - RESOURCE_REGION
48 | - RESOURCE_CONFIG
49 | - RESOURCE_TAGS
50 | - STATUS
51 | - KEYS
52 | - PROPS
53 |
54 | - display_name: AWS Config Lambda List Functions
55 | name: LW_CFG_AWS_LAMBDA_LIST_FUNCTIONS
56 | return_fields:
57 | - ARN
58 | - API_KEY
59 | - SERVICE
60 | - ACCOUNT_ID
61 | - ACCOUNT_ALIAS
62 | - RESOURCE_TYPE
63 | - RESOURCE_ID
64 | - RESOURCE_REGION
65 | - RESOURCE_CONFIG
66 | - RESOURCE_TAGS
67 | - STATUS
68 | - KEYS
69 | - PROPS
70 |
71 | - display_name: User Logins
72 | name: LW_HA_USER_LOGINS
73 | return_fields:
74 | - LOGIN_TIME
75 | - LOGOFF_TIME
76 | - EVENT_TYPE
77 | - MID
78 | - USERNAME
79 | - HOSTNAME
80 | - IP_ADDR
81 | - TTY
82 | - UID
83 | - GID
84 |
85 | - name: LW_HE_FILES
86 | display_name: Files on host
87 | return_fields:
88 | - RECORD_CREATED_TIME
89 | - MID
90 | - PATH
91 | - FILE_NAME
92 | - INODE
93 | - FILE_TYPE
94 | - IS_LINK
95 | - LINK_DEST_PATH
96 | - LINK_ABS_DEST_PATH
97 | - OWNER_UID
98 | - OWNER_USERNAME
99 | - OWNER_GID
100 | - METADATA_HASH
101 | - FILEDATA_HASH
102 | - SIZE
103 | - BLOCK_SIZE
104 | - FILE_ACCESSED_TIME
105 | - FILE_MODIFIED_TIME
106 | - FILE_CREATED_TIME
107 | - FILE_PERMISSIONS
108 | - HARD_LINK_COUNT
109 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/helper.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from laceworksdk import LaceworkClient
4 | from laceworksdk.api import base_endpoint
5 |
6 |
7 | from . import decorators
8 | from . import plugins
9 |
10 |
11 | logger = logging.getLogger("lacework_sdk.jupyter.helper")
12 |
13 |
14 | class APIWrapper:
15 | """
16 | API Wrapper class that takes an API wrapper and decorates functions.
17 | """
18 |
19 | def __init__(self, api_wrapper, wrapper_name):
20 | self._api_wrapper = api_wrapper
21 | self._api_name = wrapper_name
22 |
23 | for fname in [f for f in dir(api_wrapper) if not f.startswith("_")]:
24 | func = getattr(api_wrapper, fname)
25 |
26 | decorator_plugin = plugins.PLUGINS.get(
27 | f"{self._api_name}.{fname}")
28 | if decorator_plugin:
29 | setattr(
30 | self,
31 | fname,
32 | decorators.plugin_decorator(func, decorator_plugin))
33 | else:
34 | setattr(self, fname, decorators.dataframe_decorator(func))
35 |
36 |
37 | class LaceworkJupyterClient:
38 | """
39 | Lacework API client wrapped up for Jupyter usage.
40 |
41 | This is a simple class that acts as a Jupyter wrapper around the
42 | Python Lacework SDK. It simply wraps the SDK functions to return
43 | a DataFrame instead of a dict when calling API functions.
44 | """
45 |
46 | def __init__(
47 | self, api_key=None, api_secret=None, account=None,
48 | subaccount=None, instance=None, base_domain=None,
49 | profile=None):
50 |
51 | self.sdk = LaceworkClient(
52 | api_key=api_key, api_secret=api_secret,
53 | account=account, subaccount=subaccount,
54 | instance=instance, base_domain=base_domain,
55 | profile=profile)
56 |
57 | wrappers = [w for w in dir(self.sdk) if not w.startswith("_")]
58 | for wrapper in wrappers:
59 | if wrapper == 'subaccount':
60 | continue
61 |
62 | wrapper_object = getattr(self.sdk, wrapper)
63 | api_wrapper = APIWrapper(wrapper_object, wrapper_name=wrapper)
64 |
65 | save_wrapper = True
66 |
67 | for subwrapper in dir(wrapper_object):
68 | if subwrapper.startswith("_"):
69 | continue
70 | subwrapper_object = getattr(wrapper_object, subwrapper)
71 | if isinstance(subwrapper_object, base_endpoint.BaseEndpoint):
72 | save_wrapper = False
73 | subapi_wrapper = APIWrapper(
74 | subwrapper_object, wrapper_name=f"{wrapper}/{subwrapper}")
75 | setattr(wrapper_object, subwrapper, subapi_wrapper)
76 |
77 | if save_wrapper:
78 | setattr(self, wrapper, api_wrapper)
79 | else:
80 | setattr(self, wrapper, wrapper_object)
81 |
82 | @property
83 | def subaccount(self):
84 | """Returns the subaccount that is in use."""
85 | return self.sdk.subaccount
86 |
87 | @subaccount.setter
88 | def subaccount(self, subaccount):
89 | """Changes the subaccount that is in use."""
90 | if subaccount == self.subaccount:
91 | return
92 |
93 | self.sdk.set_subaccount(subaccount)
94 |
95 | def __enter__(self):
96 | """
97 | Support the with statement in python.
98 | """
99 | return self
100 |
101 | def __exit__(self, exc_type, exc_val, exc_tb):
102 | """
103 | Support the with statement in python.
104 | """
105 | pass
106 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/manager.py:
--------------------------------------------------------------------------------
1 | """A manager that manages various plugins in the helper."""
2 |
3 |
4 | class LaceworkManager:
5 | """Simple plugin management class."""
6 |
7 | # Dictionaries holding the registered functions.
8 | _plugins = {}
9 | _features = {}
10 |
11 | @classmethod
12 | def add_feature(cls, feature_fn, feature_name):
13 | """
14 | Add a feature to the registration.
15 |
16 | :param func feature_fn: The function to be registered.
17 | :param str feature_name: The name of the function.
18 | :raises ValueError: If the function is already registered.
19 | """
20 | feature_name = feature_name.lower()
21 |
22 | if feature_name == 'ctx':
23 | raise ValueError(
24 | 'Feature ctx is a reserved name for a feature.')
25 |
26 | if feature_name in cls._features:
27 | raise ValueError(
28 | 'Feature {0:s} is already registered as a feature.'.format(
29 | feature_name))
30 | cls._features[feature_name] = feature_fn
31 |
32 | @classmethod
33 | def get_features(cls):
34 | """
35 | Yields a tuple with the feature function and name.
36 |
37 | :yields: A tuple two items, feature function and feature name.
38 | A tuple is yielded for each registered feature in the system.
39 | """
40 | for feature_fn, feature_name in cls._features.items():
41 | yield (feature_name, feature_fn)
42 |
43 |
44 | def register_feature(fn, name=""):
45 | """
46 | Decorator that can be used to register a feature.
47 |
48 | :param function fn: The function to register.
49 | :param str name: Optional string with the name of the function
50 | as it should be registered. If not provided the name of the
51 | function is used.
52 | """
53 | if not name:
54 | name = fn.__name__
55 |
56 | LaceworkManager.add_feature(fn, name)
57 | return fn
58 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | """A simple loading of plugins."""
2 |
3 | from . import alerts
4 | from . import alert_rules
5 | from . import datasources
6 |
7 |
8 | PLUGINS = {
9 | 'alerts.get': alerts.process_alerts,
10 | 'alert_rules.get': alert_rules.process_alert_rules,
11 | 'datasources.list_data_sources': datasources.process_list_data_sources,
12 | 'datasources.get_datasource': datasources.process_datasource_schema,
13 | }
14 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/plugins/alert_rules.py:
--------------------------------------------------------------------------------
1 | """An output plugin for the Lacework Alert Rules API."""
2 |
3 |
4 | import pandas as pd
5 |
6 |
7 | def process_alert_rules(data):
8 | """
9 | Returns a Pandas DataFrame from the API call.
10 |
11 | :return: A pandas DataFrame.
12 | """
13 | data_dicts = data.get("data", [])
14 | lines = []
15 | for data_dict in data_dicts:
16 | filter_dict = data_dict.get("filters", {})
17 | filter_dict["mcGuid"] = data_dict.get("mcGuid")
18 | filter_dict["intgGuidList"] = data_dict.get("intgGuidList")
19 | filter_dict["type"] = data_dict.get("type")
20 | lines.append(filter_dict)
21 | return pd.DataFrame(lines)
22 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/plugins/alerts.py:
--------------------------------------------------------------------------------
1 | """An output plugin for the Lacework Alerts API."""
2 |
3 |
4 | import pandas as pd
5 |
6 |
7 | def process_alerts(data):
8 | """
9 | Returns a Pandas DataFrame from the API call.
10 |
11 | :return: A pandas DataFrame.
12 | """
13 | data_dicts = data.get("data", [])
14 | lines = []
15 | for data_dict in data_dicts:
16 | info = data_dict.pop("alertInfo", {})
17 | # Add Prefix.
18 | new_info = {
19 | f"info{key.capitalize()}": value for key, value in info.items()}
20 | data_dict.update(new_info)
21 | lines.append(data_dict)
22 | return pd.DataFrame(lines)
23 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/plugins/datasources.py:
--------------------------------------------------------------------------------
1 | """An output plugin for the Lacework DataSource API."""
2 |
3 |
4 | import pandas as pd
5 |
6 |
7 | def process_list_data_sources(data):
8 | """
9 | Returns a Pandas DataFrame from the API call.
10 |
11 | :return: A pandas DataFrame.
12 | """
13 | lines = [{'name': x, 'description': y} for x, y in data]
14 | return pd.DataFrame(lines)
15 |
16 |
17 | def process_datasource_schema(data):
18 | """
19 | Returns a Pandas DataFrame from the output of the API call.
20 |
21 | :return: A pandas DataFrame.
22 | """
23 | data_dict = data.get('data', {})
24 | schemas = data_dict.get('resultSchema', [])
25 |
26 | return pd.DataFrame(schemas)
27 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/plugins/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Simple utility class for the helper plugins.
3 | """
4 |
5 |
6 | def get_data_dicts(data):
7 | """
8 | Yields data dicts from data input.
9 | """
10 | if isinstance(data, dict):
11 | for data_dict in data.get("data", []):
12 | yield data_dict
13 | else:
14 | for data_item in data:
15 | for data_dict in data_item.get("data", []):
16 | if isinstance(data_dict, dict):
17 | yield data_dict
18 |
--------------------------------------------------------------------------------
/jupyter/laceworkjupyter/version.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.1.5'
2 |
3 |
4 | def get_version():
5 | """Returns the version information."""
6 | return __version__
7 |
--------------------------------------------------------------------------------
/jupyter/requirements.txt:
--------------------------------------------------------------------------------
1 | laceworksdk>=0.9.23
2 | pandas>=1.0.1
3 | pyyaml>=5.4.1
4 | ipywidgets>=7.6.5
5 | mitreattack-python>=1.4.2
6 |
--------------------------------------------------------------------------------
/jupyter/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
--------------------------------------------------------------------------------
/jupyter/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import find_packages, setup
4 |
5 | from laceworkjupyter import version
6 |
7 |
8 | PACKAGE_NAME = "laceworkjupyter"
9 |
10 | project_root = os.path.abspath(os.path.dirname(__file__))
11 |
12 | # Read the contents of README.md file
13 | with open(os.path.join(project_root, "README.md"), encoding="utf-8") as f:
14 | long_description = f.read()
15 |
16 | setup(
17 | name="laceworkjupyter",
18 | packages=find_packages(include=["laceworkjupyter", "laceworkjupyter.*"]),
19 | license="MIT",
20 | description=(
21 | "Community-developed Jupyter helper for the Lacework Python SDK"),
22 | long_description=long_description,
23 | long_description_content_type="text/markdown",
24 | author="Kristinn Gudjonsson",
25 | author_email="kristinn.gudjonsson@lacework.net",
26 | version=version.get_version(),
27 | url="https://github.com/lacework/python-sdk",
28 | download_url="https://pypi.python.org/pypi/laceworkjupyter",
29 | keywords=[
30 | "lacework", "api", "sdk", "python", "api", "jupyter", "notebook"],
31 | include_package_data=True,
32 | install_requires=[
33 | "python-dotenv",
34 | "requests",
35 | "pyyaml",
36 | "laceworksdk",
37 | "ipywidgets",
38 | "mitreattack-python",
39 | "pandas"
40 | ],
41 | classifiers=[
42 | "Development Status :: 4 - Beta",
43 | "Intended Audience :: Developers",
44 | "Topic :: Software Development :: Build Tools",
45 | "License :: OSI Approved :: MIT License",
46 | "Programming Language :: Python :: 3",
47 | "Programming Language :: Python :: 3.6",
48 | "Programming Language :: Python :: 3.7",
49 | "Programming Language :: Python :: 3.8",
50 | "Programming Language :: Python :: 3.9",
51 | ],
52 | )
53 |
--------------------------------------------------------------------------------
/jupyter/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
--------------------------------------------------------------------------------
/jupyter/tests/laceworkjupyter/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
--------------------------------------------------------------------------------
/jupyter/tests/laceworkjupyter/test_accessors.py:
--------------------------------------------------------------------------------
1 | """
2 | Test file for the local accessors.
3 | """
4 | import pandas as pd
5 |
6 | from laceworkjupyter import accessors # noqa: F401
7 |
8 |
9 | def test_decode_accessor():
10 | """
11 | Tests the decode accessor.
12 | """
13 | lines = [
14 | {
15 | 'value': 12, 'some_string': 'VGhpcyBpcyBhIHN0cmluZw==',
16 | 'uri': 'http://mbl.is/%3Fstuff=r+1%20af'
17 | }, {
18 | 'value': 114, 'some_string': 'VGhpcyBpcyBhIGEgc2VjcmV0',
19 | 'uri': 'http://mbl.is/%3Fsfi=r+1%20af'
20 | },
21 | ]
22 | frame = pd.DataFrame(lines)
23 |
24 | decoded_series = frame.some_string.decode.base64()
25 | discovered_set = set(list(decoded_series.values))
26 |
27 | expected_set = set([
28 | 'This is a a secret', 'This is a string'])
29 |
30 | assert expected_set == discovered_set
31 |
32 | unquoted_series = frame.uri.decode.url_unquote()
33 | unquoted_set = set(list(unquoted_series.values))
34 |
35 | expected_set = set([
36 | 'http://mbl.is/?stuff=r 1 af',
37 | 'http://mbl.is/?sfi=r 1 af'])
38 |
39 | assert expected_set == unquoted_set
40 |
--------------------------------------------------------------------------------
/jupyter/tests/laceworkjupyter/test_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python Jupyter notebook
4 | assistant interacting with Lacework APIs.
5 | """
6 |
7 | import datetime
8 |
9 | import pandas
10 | from pandas._testing import assert_frame_equal
11 | import pytest
12 |
13 | from laceworkjupyter import utils
14 |
15 |
16 | TIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ"
17 |
18 |
19 | def test_parse_data_offset():
20 | """Test the data offset operation."""
21 | def get_diff(offset_string):
22 | start_date, end_date = utils.parse_date_offset(offset_string)
23 | start_date_dt = datetime.datetime.strptime(start_date, TIME_FMT)
24 | end_date_dt = datetime.datetime.strptime(end_date, TIME_FMT)
25 |
26 | return end_date_dt - start_date_dt
27 |
28 | diff_dt = get_diff("LAST 3 DAYS")
29 | assert diff_dt.days == 3
30 |
31 | diff_dt = get_diff("LAST 23 MINUTES")
32 | assert diff_dt.total_seconds() == (23 * 60)
33 |
34 | diff_dt = get_diff("LAST 16 SECONDS")
35 | assert diff_dt.total_seconds() == 16
36 |
37 | with pytest.raises(ValueError):
38 | _ = utils.parse_date_offset("HIMA")
39 |
40 | with pytest.raises(ValueError):
41 | _ = utils.parse_date_offset("FIRST 132 DAYS")
42 |
43 | with pytest.raises(ValueError):
44 | _ = utils.parse_date_offset("LAST 132")
45 |
46 | with pytest.raises(ValueError):
47 | _ = utils.parse_date_offset("LAST 132 YEARS")
48 |
49 |
50 | def test_flatten_json_output():
51 | """Test flattening out a JSON structure."""
52 | test_json = {
53 | "items": [{"value": 1243}, {"value": 634}],
54 | "stuff": "This is stuff",
55 | "value": 2355
56 | }
57 |
58 | flattened = list(utils.flatten_json_output(test_json))[0]
59 | expected_json = {
60 | "items_1.value": 1243,
61 | "items_2.value": 634,
62 | "stuff": "This is stuff",
63 | "value": 2355
64 | }
65 | assert flattened == expected_json
66 |
67 | flattened = list(utils.flatten_json_output(test_json, pre_key="foobar"))[0]
68 | new_expected = {
69 | f"foobar.{key}": value for key, value in expected_json.items()}
70 | assert flattened == new_expected
71 |
72 | flattened = list(utils.flatten_json_output(test_json, lists_to_rows=True))
73 | expected_json = [
74 | {
75 | "items.value": 1243,
76 | "stuff": "This is stuff",
77 | "value": 2355
78 | }, {
79 | "items.value": 634,
80 | "stuff": "This is stuff",
81 | "value": 2355
82 | }
83 | ]
84 |
85 | assert flattened == expected_json
86 |
87 |
88 | def test_flatten_data_frame():
89 | """Generate a dataframe and flatten it."""
90 | lines = [
91 | {"first": 234, "second": 583, "third": {
92 | "item": 3, "another": "fyrirbaeri"}},
93 | {"first": 214, "second": 529, "third": {
94 | "item": 23, "another": "blanda"}},
95 | {"first": 134, "second": 545, "third": {
96 | "item": 43, "another": "ymist"}},
97 | {"first": 452, "second": 123, "third": {
98 | "item": 95, "another": "dot"}},
99 | ]
100 |
101 | data_frame = pandas.DataFrame(lines)
102 | flattened_frame = utils.flatten_data_frame(data_frame)
103 |
104 | expected_lines = [
105 | {
106 | "first": 234, "second": 583,
107 | "third.item": 3, "third.another": "fyrirbaeri"},
108 | {
109 | "first": 214, "second": 529, "third.item": 23,
110 | "third.another": "blanda"},
111 | {
112 | "first": 134, "second": 545, "third.item": 43,
113 | "third.another": "ymist"},
114 | {
115 | "first": 452, "second": 123, "third.item": 95,
116 | "third.another": "dot"},
117 | ]
118 | expected_frame = pandas.DataFrame(expected_lines)
119 |
120 | assert_frame_equal(flattened_frame, expected_frame)
121 |
--------------------------------------------------------------------------------
/laceworksdk/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | from importlib_metadata import version
7 |
8 | import logging
9 |
10 | from .api import LaceworkClient # noqa: F401
11 | from .exceptions import ApiError, LaceworkSDKException # noqa: F401
12 |
13 | __version__ = version(__name__)
14 |
15 | # Initialize Package Logging
16 | logger = logging.getLogger(__name__)
17 | logger.addHandler(logging.NullHandler())
18 |
--------------------------------------------------------------------------------
/laceworksdk/api/read_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """The base read class for the Lacework Python SDK"""
3 | from laceworksdk.api.base_endpoint import BaseEndpoint
4 |
5 |
6 | class ReadEndpoint(BaseEndpoint):
7 | """A class used to implement Read functionality for Lacework API Endpoints."""
8 |
9 | # If defined, this is the resource used in the URL path
10 | RESOURCE = ""
11 |
12 | def __init__(self, session, object_type, endpoint_root="/api/v2"):
13 | """
14 | Initialize the ReadEndpoint Class.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class.
18 | object_type (str): The Lacework object type to use.
19 | endpoint_root (str, optional): The URL endpoint root to use.
20 | """
21 | super().__init__(session, object_type, endpoint_root)
22 |
23 | def get(self, id=None, resource=None, **request_params):
24 | """A method to get objects.
25 |
26 | Args:
27 | id (str): A string representing the object ID.
28 | resource (str): The Lacework API resource type to get.
29 | request_params (dict): Use to pass any additional parameters the API
30 |
31 | Returns:
32 | dict: the requested o
33 | """
34 |
35 | if not resource and self.RESOURCE:
36 | resource = self.RESOURCE
37 |
38 | params = self._build_dict_from_items(request_params)
39 |
40 | response = self._session.get(
41 | self._build_url(id=id, resource=resource), params=params
42 | )
43 |
44 | return response.json()
45 |
--------------------------------------------------------------------------------
/laceworksdk/api/search_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """The base search class for the Lacework Python SDK"""
3 | from laceworksdk.api.base_endpoint import BaseEndpoint
4 |
5 |
6 | class SearchEndpoint(BaseEndpoint):
7 | """A class used to implement Search functionality for Lacework API Endpoints."""
8 |
9 | # If defined, this is the resource used in the URL path
10 | RESOURCE = ""
11 |
12 | def __init__(self, session, object_type, endpoint_root="/api/v2"):
13 | """
14 | Initialize the SearchEndpoint class.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class.
18 | object_type (str): The Lacework object type to use.
19 | endpoint_root (str, optional): The URL endpoint root to use.
20 | """
21 | super().__init__(session, object_type, endpoint_root)
22 |
23 | def search(self, json=None, resource=None):
24 | """A method to search objects.
25 |
26 | See the API documentation for this API endpoint for valid fields to search against.
27 |
28 | NOTE: While the "value" and "values" fields are marked as "optional" you must use one of them,
29 | depending on the operation you are using.
30 |
31 | Args:
32 | json (dict): The desired search parameters: \n
33 | - timeFilter (dict, optional): A dict containing the time frame for the search:\n
34 | - startTime (str): The start time for the search
35 | - endTime (str): The end time for the search
36 |
37 | - filters (list of dict, optional): Filters based on field contents:\n
38 | - field (str): The name of the data field to which the condition applies\n
39 | - expression (str): The comparison operator for the filter condition. Valid values are:\n
40 |
41 | "eq", "ne", "in", "not_in", "like", "ilike", "not_like", "not_ilike", "not_rlike", "rlike", "gt", "ge", \
42 | "lt", "le", "between"\n
43 |
44 | - value (str, optional): The value that the condition checks for in the specified field. Use this attribute \
45 | when using an operator that requires a single value.
46 | - values (list of str, optional): The values that the condition checks for in the specified field. Use this \
47 | attribute when using an operator that requires multiple values.
48 | - returns (list of str, optional): The fields to return
49 | resource (str): The Lacework API resource to search (Example: "AlertChannels")
50 |
51 | Yields:
52 | dict: returns a generator which yields a page of objects at a time as returned by the Lacework API.
53 | """
54 |
55 | if not resource and self.RESOURCE:
56 | resource = self.RESOURCE
57 |
58 | response = self._session.post(
59 | self._build_url(resource=resource, action="search"), json=json
60 | )
61 |
62 | while True:
63 | response_json = response.json()
64 | yield response_json
65 |
66 | try:
67 | next_page = (
68 | response_json.get("paging", {}).get("urls", {}).get("nextPage")
69 | )
70 | except Exception:
71 | next_page = None
72 |
73 | if next_page:
74 | response = self._session.get(next_page)
75 | else:
76 | break
77 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/__init__.py:
--------------------------------------------------------------------------------
1 | """Classes to access the Lacework V2 API endpoints"""
--------------------------------------------------------------------------------
/laceworksdk/api/v2/activities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Activities API wrapper."""
3 |
4 | from laceworksdk.api.search_endpoint import SearchEndpoint
5 |
6 |
7 | class ActivitiesAPI:
8 | """A class used to represent the `Activities API endpoint `_
9 |
10 | Get information about network activities detected through the Lacework agent.
11 |
12 | The Activities API endpoint is a parent for different types of
13 | activities that can be queried.
14 |
15 | Attributes:
16 | ----------
17 | changed_files:
18 | A ChangedFilesAPI instance.
19 | connections:
20 | A ConnectionsAPI instance.
21 | dns:
22 | A DnsAPI instance.
23 | user_logins:
24 | A UserLoginsAPI instance.
25 |
26 | """
27 |
28 | def __init__(self, session):
29 | """Initializes the ActivitiesAPI object.
30 |
31 | Args:
32 | session(HttpSession): An instance of the HttpSession class
33 |
34 | Return:
35 | ActivitiesAPI object.
36 | """
37 | super().__init__()
38 | self._base_path = "Activities"
39 |
40 | self.changed_files = self.ChangedFilesAPI(session, self._base_path)
41 | self.connections = self.ConnectionsAPI(session, self._base_path)
42 | self.dns = self.DnsAPI(session, self._base_path)
43 | self.user_logins = self.UserLoginsAPI(session, self._base_path)
44 |
45 | class ChangedFilesAPI(SearchEndpoint):
46 | """A class used to represent the `Changed Files API endpoint `_
47 |
48 | Search for changed files in your environment
49 | """
50 |
51 | RESOURCE = "ChangedFiles"
52 |
53 | class ConnectionsAPI(SearchEndpoint):
54 | """A class used to represent the `Connections API endpoint `_
55 |
56 | Search for connections in your environment.
57 | """
58 |
59 | RESOURCE = "Connections"
60 |
61 | class DnsAPI(SearchEndpoint):
62 | """A class used to represent the `DNS Lookup API endpoint `_
63 |
64 | Search for DNS summaries in your environment.
65 | """
66 |
67 | RESOURCE = "DNSs"
68 |
69 | class UserLoginsAPI(SearchEndpoint):
70 | """A class used to represent the `UserLogins API endpoint `_
71 |
72 | Search for user logins in your environment.
73 | """
74 |
75 | RESOURCE = "UserLogins"
76 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/agent_access_tokens.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework AgentAccessTokens API wrapper."""
3 |
4 | from laceworksdk.api.crud_endpoint import CrudEndpoint
5 |
6 |
7 | class AgentAccessTokensAPI(CrudEndpoint):
8 | """A class used to represent the `Agent Access Tokens API endpoint `_
9 |
10 | To connect to the Lacework instance, Lacework agents require an agent access token.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the AgentAccessTokensAPI object.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | AgentAccessTokensAPI: An AgentAccessTokensAPI object.
21 |
22 | """
23 | super().__init__(session, "AgentAccessTokens")
24 |
25 | def create(self, alias, enabled, props=None, **request_params):
26 | """A method to create a new agent access token.
27 |
28 | Args:
29 | alias (str): A string representing the name you wish to give to the created token.
30 | enabled (bool|int): A boolean/integer representing whether the token is enabled.
31 | props (dict, optional): A dict containing optional values for the following fields:
32 | - description(str, optional): a description of the token
33 | - os(str, optional): the operating system
34 | - subscription(str, optional): The subscription level of the token. Valid values are:
35 | "standard", "professional", "enterprise"
36 | request_params (dict): Use to pass any additional parameters the API
37 |
38 | Returns:
39 | dict: The new access token
40 | """
41 | return super().create(
42 | token_alias=alias,
43 | token_enabled=int(bool(enabled)),
44 | props=props,
45 | **request_params,
46 | )
47 |
48 | def get_by_id(self, id):
49 | """A method to get an agent access token by its ID.
50 |
51 | Args:
52 | id (str): A string representing the object ID.
53 |
54 | Returns:
55 | dict: a JSON object containing info regarding the requested access token
56 |
57 | """
58 | return self.get(id=id)
59 |
60 | def update(self, id, token_enabled=None, props=None, **request_params):
61 | """A method to update an agent access token.
62 |
63 | Args:
64 | id (str): A string representing the object ID.
65 | token_enabled (bool|int, optional): A boolean/integer representing whether the object is enabled.
66 | props (dict, optional): A dict containing optional values for the following fields:\n
67 |
68 | - description (str, optional): a description of the token
69 | - os (str, optional): the operating system
70 | - subscription (str, optional): The subscription level of the token. Valid values are:
71 | "standard", "professional", "enterprise"
72 |
73 | request_params (dict): Use to pass any additional parameters the API
74 |
75 | Returns:
76 | dict: The updated access token.
77 |
78 | """
79 | if token_enabled is not None:
80 | token_enabled = int(bool(token_enabled))
81 |
82 | return super().update(
83 | id=id, token_enabled=token_enabled, props=props, **request_params
84 | )
85 |
86 | def delete(self):
87 | """
88 | Lacework does not currently allow for agent access tokens to be deleted.
89 | """
90 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/agent_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework AgentInfo API wrapper."""
3 |
4 | from laceworksdk.api.search_endpoint import SearchEndpoint
5 |
6 |
7 | class AgentInfoAPI(SearchEndpoint):
8 | """A class used to represent the `Agent Info API endpoint `_
9 |
10 | View and verify information about all agents.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the AgentInfo API object.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | AgentInfoAPI: an AgentInfoAPI object.
21 | """
22 | super().__init__(session, "AgentInfo")
23 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/alert_rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework AlertRules API wrapper."""
3 |
4 | from laceworksdk.api.crud_endpoint import CrudEndpoint
5 |
6 |
7 | class AlertRulesAPI(CrudEndpoint):
8 | """A class used to represent the `Alert Rules API endpoint `_
9 |
10 | Lacework combines alert channels and alert rules to provide a flexible method for routing alerts. For alert
11 | channels, you define information about where to send alerts, such as to Jira, Slack, or email. For alert rules,
12 | you define information about which alert types to send, such as critical and high severity compliance alerts.
13 | """
14 |
15 | def __init__(self, session):
16 | """Initializes the AlertRulesAPI object.
17 |
18 | Args:
19 | session (HttpSession): An instance of the HttpSession class
20 |
21 | Returns:
22 | AlertRulesAPI: returns an AlertRulesAPI object
23 | """
24 | super().__init__(session, "AlertRules")
25 |
26 | def create(self, type, filters, intg_guid_list, **request_params):
27 | """A method to create new Alert Rules.
28 |
29 | Args:
30 | type (str): The type of the alert rule. Valid values are: "Event"
31 | filters (dict): The alert rule definition. See the `API docs `_ for valid values.
32 | intg_guid_list (list of str): A list of GUIDs representing the alert channels to use.
33 |
34 | request_params (dict, optional): Use to pass any additional parameters the API
35 |
36 | Returns:
37 | dict: The new rule.
38 | """
39 | return super().create(
40 | type=type,
41 | filters=self._format_filters(filters),
42 | intg_guid_list=intg_guid_list,
43 | **request_params,
44 | )
45 |
46 | def get(self, guid=None):
47 | """A method to get AlertRules objects.
48 |
49 | Args:
50 | guid (str): The alert rule GUID to retrieve.
51 |
52 | Returns:
53 | dict: The alert rule(s)
54 |
55 | """
56 | return super().get(id=guid)
57 |
58 | def get_by_guid(self, guid):
59 | """A method to get an AlertRules object by GUID.
60 |
61 | Args:
62 | guid (str): The alert rule GUID.
63 |
64 | Returns:
65 | dict: The alert rule
66 |
67 | """
68 | return self.get(guid=guid)
69 |
70 | def update(self, guid, filters=None, intg_guid_list=None, **request_params):
71 | """A method to update an AlertRules object.
72 |
73 | Args:
74 | guid (str): The Alert Rule GUID you wish to update.
75 | filters (dict, optional): The alert rule definition. See the `API docs `_ for valid values.
76 | intg_guid_list (list of str, optional): A list of GUIDs representing the alert channels to use.
77 | request_params (dict, optional): Use to pass any additional parameters the API
78 |
79 | Returns:
80 | dict: The updated alert rule
81 |
82 | """
83 | return super().update(
84 | id=guid,
85 | filters=self._format_filters(filters),
86 | intg_guid_list=intg_guid_list,
87 | **request_params,
88 | )
89 |
90 | def delete(self, guid):
91 | """A method to delete an AlertRules object.
92 |
93 | Args:
94 | guid (str): The alert rule GUID.
95 |
96 | Returns:
97 | requests.models.Response: a Requests response object containing the response code
98 |
99 | """
100 | return super().delete(id=guid)
101 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/audit_logs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework AuditLogs API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class AuditLogsAPI(BaseEndpoint):
8 | """A class used to represent the `Audit Log API endpoint `_
9 |
10 | Get audit logs.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the AuditLogsAPI object.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | AuditLogsAPI: An instance of this class.
21 | """
22 | super().__init__(session, "AuditLogs")
23 |
24 | def get(self, start_time=None, end_time=None, **request_params):
25 | """A method to get audit logs.
26 |
27 | Args:
28 | start_time (str): A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from.
29 | end_time (str): A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at.
30 | request_params (dict, optional): Use to pass any additional parameters the API
31 |
32 | Returns:
33 | dict: The audit logs for the requested time period.
34 | """
35 | params = self._build_dict_from_items(
36 | request_params, start_time=start_time, end_time=end_time
37 | )
38 |
39 | response = self._session.get(self._build_url(), params=params)
40 |
41 | return response.json()
42 |
43 | def search(self, json=None):
44 | """A method to search audit logs.
45 |
46 | See the API documentation for this API endpoint for valid fields to search against.
47 |
48 | NOTE: While the "value" and "values" fields are marked as "optional" you must use one of them,
49 | depending on the operation you are using.
50 |
51 | Args:
52 | json (list of dicts): A list of dictionaries containing the desired search parameters: \n
53 | - field (str): The name of the data field to which the condition applies\n
54 | - expression (str): The comparison operator for the filter condition. Valid values are:\n
55 | "eq", "ne", "in", "not_in", "like", "ilike", "not_like", "not_ilike", "not_rlike", "rlike", "gt", "ge", \
56 | "lt", "le", "between"
57 | - value (str, optional): The value that the condition checks for in the specified field. Use this attribute \
58 | when using an operator that requires a single value.
59 | - values (list of str, optional): The values that the condition checks for in the specified field. Use this \
60 | attribute when using an operator that requires multiple values.
61 |
62 | Yields:
63 | dict: returns a generator which yields a page of objects at a time as returned by the Lacework API.
64 | """
65 |
66 | response = self._session.post(self._build_url(action="search"), json=json)
67 | return response.json()
68 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/configs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Configs API wrapper."""
3 |
4 | from laceworksdk.api.read_endpoint import ReadEndpoint
5 | from laceworksdk.api.search_endpoint import SearchEndpoint
6 |
7 |
8 | class ConfigsAPI:
9 | """A class used to represent the `Configs API endpoint `_
10 |
11 | Get information about compliance configurations.
12 |
13 | The Configs API endpoint is a parent for different types of configs that can be queried.
14 |
15 | Attributes:
16 | compliance_evaluations (ComplianceEvaluationsAPI): A ComplianceEvaluationsAPI instance.
17 | azure_subscriptions (AzureSubscriptions): An AzureSubscriptions instance.
18 | gcp_projects (GcpProjects): A GcpProjects instance.
19 | """
20 |
21 | def __init__(self, session):
22 | """Initializes the ConfigsAPI object.
23 |
24 | Args:
25 | session (HttpSession): An instance of the HttpSession class
26 |
27 | Returns:
28 | ConfigsAPI: An instance of this class..
29 | """
30 | super().__init__()
31 | self._base_path = "Configs"
32 |
33 | self.azure_subscriptions = self.AzureSubscriptions(session, self._base_path)
34 | self.compliance_evaluations = self.ComplianceEvaluationsAPI(session, self._base_path)
35 | self.gcp_projects = self.GcpProjects(session, self._base_path)
36 |
37 |
38 | class AzureSubscriptions(ReadEndpoint):
39 | """A class used to represent the Azure Subscriptions API endpoint.
40 |
41 | Get a list of Azure subscription IDs for an entire account or for a specific Azure tenant.
42 |
43 | """
44 |
45 | RESOURCE = "AzureSubscriptions"
46 |
47 |
48 | class GcpProjects(ReadEndpoint):
49 | """A class used to represent the GCP Projects API endpoint."""
50 |
51 | RESOURCE = "GcpProjects"
52 |
53 |
54 | class ComplianceEvaluationsAPI(SearchEndpoint):
55 | """A class used to represent the Compliance Evaluations API endpoint."""
56 |
57 | RESOURCE = "ComplianceEvaluations"
58 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/contract_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework ContractInfo API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class ContractInfoAPI(BaseEndpoint):
8 | """A class used to represent the `Contract Info API endpoint `_
9 |
10 | Get Lacework contract information.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the ContractInfoAPI object.
15 |
16 | Args:
17 | session(HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | ContractInfoAPI: An instance of this class.
21 |
22 | """
23 | super().__init__(session, "ContractInfo")
24 |
25 | def get(self, **request_params):
26 | """A method to get contract info
27 |
28 | Returns:
29 | dict: Contract info for the lacework instance.
30 | request_params (dict, optional): Use to pass any additional parameters the API
31 |
32 | """
33 | params = self._build_dict_from_items(request_params)
34 |
35 | response = self._session.get(self._build_url(), params=params)
36 |
37 | return response.json()
38 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/datasources.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Datasources API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class DatasourcesAPI(BaseEndpoint):
8 | """A class used to represent the `Datasources API endpoint `_
9 |
10 | Get schema details for all datasources that you can query using LQL.
11 | """
12 |
13 | _DEFAULT_DESCRIPTION = "No description available."
14 |
15 | def __init__(self, session):
16 | """Initializes the Datasources object.
17 |
18 | Args:
19 | session (HttpSession): An instance of the HttpSession class
20 |
21 | Returns:
22 | DatasourcesAPI: An instance of this class
23 | """
24 | super().__init__(session, "Datasources")
25 |
26 | def get(self):
27 | """A method to get Datasources.
28 |
29 | Returns:
30 | dict: All datasources
31 | """
32 | response = self._session.get(self._build_url())
33 | return response.json()
34 |
35 | def get_datasource(self, datasource):
36 | """A method to get the schema for a particular datasource.
37 |
38 | Args:
39 | datasource (str): The name of the datasource schema get.
40 |
41 | Returns:
42 | dict: The datasource schema.
43 | """
44 | return self._session.get(self._build_url(resource=datasource)).json()
45 |
46 | def list_data_sources(self):
47 | """A method to list the datasources that are available.
48 |
49 | Returns:
50 | list of tuples: Each tuple has two entries, source name and description.
51 | """
52 | response_json = self.get()
53 |
54 | return_sources = []
55 | data_sources = response_json.get("data", [])
56 | for data_source in data_sources:
57 | description = data_source.get("description", self._DEFAULT_DESCRIPTION)
58 | if description == "None":
59 | description = self._DEFAULT_DESCRIPTION
60 |
61 | return_sources.append((data_source.get("name", "No name"), description))
62 |
63 | return return_sources
64 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/events.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Events API wrapper."""
3 |
4 | from laceworksdk.api.search_endpoint import SearchEndpoint
5 |
6 |
7 | class EventsAPI(SearchEndpoint):
8 | """A class used to represent the `Events API endpoint `_
9 |
10 | View and verify the evidence or observation details of individual events.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the EventsAPI object.
15 |
16 | Args:
17 | session(HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | EventsAPI: An instance of this class
21 |
22 | """
23 | super().__init__(session, "Events")
24 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/inventory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Inventory API wrapper."""
3 |
4 | from laceworksdk.api.search_endpoint import SearchEndpoint
5 |
6 |
7 | class InventoryAPI(SearchEndpoint):
8 | """A class used to represent the `Inventory API endpoint `_
9 |
10 | View and monitor in-use cloud resources' risk, compliance, and configuration changes.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the Inventory API object.
15 |
16 | Args:
17 | session (HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | InventoryAPI: An instance of this class.
21 |
22 | """
23 | super().__init__(session, "Inventory")
24 |
25 | def scan(self, csp):
26 | """A method to trigger a resource inventory scan.
27 |
28 | Args:
29 | csp (string): The cloud service provider to run the scan on. Valid values are: "AWS" "Azure" "GCP"
30 |
31 | Returns:
32 | dict: Status of scan
33 |
34 | """
35 | params = self._build_dict_from_items(csp=csp)
36 |
37 | response = self._session.post(self._build_url(action="scan"), params=params)
38 |
39 | return response.json()
40 |
41 | def status(self, csp):
42 | """A method to get the status of a Resource Inventory scan.
43 |
44 | Args:
45 | csp (string): The cloud service provider to run the scan on. Valid values are: "AWS" "Azure" "GCP"
46 |
47 | Returns:
48 | dict: Status of scan
49 |
50 | """
51 | params = self._build_dict_from_items(csp=csp)
52 |
53 | response = self._session.get(self._build_url(action="scan"), params=params)
54 |
55 | return response.json()
56 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/organization_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework OrganizationInfo API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class OrganizationInfoAPI(BaseEndpoint):
8 | """A class used to represent the `Organization Info API endpoint `_
9 |
10 | Return information about whether the Lacework account is an organization account and, if it is, what the organization account URL is.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the OrganizationInfoAPI object.
15 |
16 | Args:
17 | session: An instance of the HttpSession class
18 |
19 | Returns:
20 | OrganizationInfoAPI object.
21 |
22 | """
23 | super().__init__(session, "OrganizationInfo")
24 |
25 | def get(self):
26 | """A method to get organization info.
27 |
28 | Returns:
29 | dict: Organization info
30 |
31 | """
32 | response = self._session.get(self._build_url())
33 |
34 | return response.json()
35 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/reports.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Reports API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class ReportsAPI(BaseEndpoint):
8 | """A class used to represent the `Reports API endpoint `_
9 |
10 | Lacework combines details about non-compliant resources that are in violation into reports. You must configure at
11 | least one cloud integration with AWS, Azure, or GCP to receive these reports.
12 | """
13 |
14 | def __init__(self, session):
15 | """Initializes the ReportsAPI object.
16 |
17 | Args:
18 | session(HttpSession): An instance of the HttpSession class
19 |
20 | Returns:
21 | ReportsAPI: An instance of this class
22 | """
23 | super().__init__(session, "Reports")
24 |
25 | def get(
26 | self,
27 | primary_query_id=None,
28 | secondary_query_id=None,
29 | format=None,
30 | report_type=None,
31 | **request_params,
32 | ):
33 | """A method to get Reports objects.
34 |
35 | Args:
36 | primary_query_id (str): The primary ID that is used to fetch the report. (AWS Account ID or Azure Tenant ID)
37 | secondary_query_id (str): The secondary ID that is used to fetch the report. (GCP Project ID or Azure Subscription ID)
38 | format (str, optional): The format of the report. Valid values: "csv", "html", "json", "pdf"
39 | report_type (str): The type of the report. See `available reports `_ for a list of report types.\
40 | Valid values are in the "API Format" column.
41 | request_params (dict, optional): Use to pass any additional parameters the API
42 |
43 | Returns:
44 | dict: The details of the report
45 | """
46 |
47 | params = self._build_dict_from_items(
48 | primary_query_id=primary_query_id,
49 | secondary_query_id=secondary_query_id,
50 | format=format,
51 | report_type=report_type,
52 | **request_params,
53 | )
54 |
55 | response = self._session.get(self._build_url(), params=params)
56 |
57 | if format == "json":
58 | return response.json()
59 | else:
60 | return response.content
61 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/schemas.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework Schemas API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class SchemasAPI(BaseEndpoint):
8 | """A class used to represent the `Schemas API endpoint `_
9 |
10 | Get details about the available Lacework schemas.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initializes the SchemasAPI object.
15 |
16 | Args:
17 | session(HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | SchemasAPI: An instance of this class
21 | """
22 | super().__init__(session, "schemas")
23 |
24 | def get(self, type=None, subtype=None):
25 | """A method to get schema objects. Using no args will get all schemas.
26 |
27 | Args:
28 | type (str, optional): The schema type to retrieve. Valid values are any API resource listed in the Lacework API\
29 | `documentation `_ .Examples include "AlertChannels", "CloudAccounts", \
30 | "AgentAccessTokens", etc..
31 | subtype (str, optional): The subtype to retrieve. Subtypes are only available for API resources that have \
32 | "type" like fields. For instance the "AlertChannels" resource has subtypes such as "AwsS3", "SlackChannel", \
33 | etc. See the Lacework API `documentation `_ for more info.
34 |
35 | Returns:
36 | dict: The requested schema
37 |
38 | """
39 | response = self._session.get(self._build_url(id=subtype, resource=type))
40 |
41 | return response.json()
42 |
43 | def get_by_subtype(self, type, subtype):
44 | """A method to fetch a specific subtype schema.
45 |
46 | Args:
47 | type (str): The schema type to retrieve. Valid values are any API resource listed in the Lacework API\
48 | `documentation `_ .Examples include "AlertChannels", "CloudAccounts", \
49 | "AgentAccessTokens", etc..
50 | subtype (str): The subtype to retrieve. Subtypes are only available for API resources that have \
51 | "type" like fields. For instance the "AlertChannels" resource has subtypes such as "AwsS3", "SlackChannel", \
52 | etc. See the Lacework API `documentation `_ for more info.
53 |
54 | Returns:
55 | dict: The requested schema
56 | """
57 | return self.get(type=type, subtype=subtype)
58 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/user_groups.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework UserGroups API wrapper (Experimental)."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class UserGroupsAPI(BaseEndpoint):
8 | """A class used to represent the `User Groups API endpoint `_ .
9 |
10 | A user group associates Lacework service and standard users with specific permissions in Lacework.
11 | """
12 |
13 | def __init__(self, session):
14 | """Initialize the UserGroupsAPI object.
15 |
16 | Args:
17 | session(HttpSession): An instance of the HttpSession class
18 |
19 | Returns:
20 | UserGroupsAPI: An instance of this class
21 | """
22 | super().__init__(session, "UserGroups")
23 |
24 | def __modify_members(self, guid, user_guids, action):
25 | json = self._build_dict_from_items(
26 | user_guids=user_guids,
27 | )
28 |
29 | response = self._session.post(
30 | self._build_url(resource=guid, action=action), json=json, params=None
31 | )
32 |
33 | return response.json()
34 |
35 | def add_users(self, guid, user_guids):
36 | """A method to add users to existing UserGroup object.
37 |
38 | Args:
39 | guid (str): The GUID of the UserGroup to modify
40 | user_guids (list of str): An array of user guids to add to the user group
41 |
42 | Returns:
43 | dict: The modified results
44 | """
45 | return self.__modify_members(guid, user_guids, "addUsers")
46 |
47 | def remove_users(self, guid, user_guids):
48 | """A method to remove users from an existing UserGroup object.
49 |
50 | Args:
51 | guid (str): The GUID of the UserGroup object to modify.
52 | user_guids (list of str): An array of user guids to remove from the user group
53 |
54 | Returns:
55 | dict: The modified results
56 |
57 | """
58 | return self.__modify_members(guid, user_guids, "removeUsers")
59 |
--------------------------------------------------------------------------------
/laceworksdk/api/v2/user_profile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Lacework UserProfile API wrapper."""
3 |
4 | from laceworksdk.api.base_endpoint import BaseEndpoint
5 |
6 |
7 | class UserProfileAPI(BaseEndpoint):
8 | """A class used to represent the `User Profile API endpoint `_ .
9 |
10 | An organization can contain multiple accounts so you can also manage components such as alerts, resource groups, \
11 | team members, and audit logs at a more granular level inside an organization.
12 | """
13 |
14 | def __init__(self, session):
15 | """Initializes the UserProfileAPI object.
16 |
17 | Args:
18 | session(HttpSession): An instance of the HttpSession class
19 |
20 | Returns:
21 | UserProfileAPI: An instance of this class
22 |
23 | """
24 | super().__init__(session, "UserProfile")
25 |
26 | def get(self, account_name=None):
27 | """A method to get Lacework sub-accounts that are managed by your organization account. Using no args will get all sub-accounts.
28 |
29 | Args:
30 | account_name (str, optional): Specify which sub-account to list.
31 |
32 | Returns:
33 | dict: Details of the requested sub-account(s)
34 |
35 | """
36 | response = self._session.get(self._build_url(), params=account_name)
37 |
38 | return response.json()
39 |
--------------------------------------------------------------------------------
/laceworksdk/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Package configuration.
4 | """
5 |
6 | # Package Constants
7 | DEFAULT_BASE_DOMAIN = "lacework.net"
8 | DEFAULT_ACCESS_TOKEN_EXPIRATION = 3600
9 | DEFAULT_SUCCESS_RESPONSE_CODES = [200, 201, 204]
10 | RATE_LIMIT_RESPONSE_CODE = 429
11 |
12 | # Environment Variable Definitions
13 | LACEWORK_ACCOUNT_ENVIRONMENT_VARIABLE = "LW_ACCOUNT"
14 | LACEWORK_SUBACCOUNT_ENVIRONMENT_VARIABLE = "LW_SUBACCOUNT"
15 | LACEWORK_API_KEY_ENVIRONMENT_VARIABLE = "LW_API_KEY"
16 | LACEWORK_API_SECRET_ENVIRONMENT_VARIABLE = "LW_API_SECRET"
17 | LACEWORK_API_TOKEN_ENVIRONMENT_VARIABLE = "LW_API_TOKEN"
18 | LACEWORK_API_BASE_DOMAIN_ENVIRONMENT_VARIABLE = "LW_BASE_DOMAIN"
19 | LACEWORK_API_CONFIG_SECTION_ENVIRONMENT_VARIABLE = "LW_PROFILE"
20 |
21 | # Config file paths.
22 | LACEWORK_CLI_CONFIG_RELATIVE_PATH = ".lacework.toml"
23 |
--------------------------------------------------------------------------------
/laceworksdk/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Package exceptions.
4 | """
5 |
6 | import logging
7 |
8 | import requests
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class LaceworkSDKException(Exception):
14 | """
15 | Base class for all lacework package exceptions.
16 | """
17 |
18 | pass
19 |
20 |
21 | class ApiError(LaceworkSDKException):
22 | """
23 | Errors returned in response to requests sent to the Lacework APIs.
24 |
25 | Several data attributes are available for inspection.
26 | """
27 |
28 | def __init__(self, response):
29 | """Create an instance of the APIError class."""
30 | assert isinstance(response, requests.Response)
31 |
32 | # Extended exception attributes
33 | self.response = response
34 | """The :class:`requests.Response` object returned from the API call."""
35 |
36 | self.request = self.response.request
37 | """The :class:`requests.PreparedRequest` of the API call."""
38 |
39 | self.status_code = self.response.status_code
40 | """The HTTP status code from the API response."""
41 |
42 | self.status = self.response.reason
43 | """The HTTP status from the API response."""
44 |
45 | self.details = None
46 | """The parsed JSON details from the API response."""
47 | if "application/json" in self.response.headers.get("Content-Type", "").lower():
48 | try:
49 | self.details = self.response.json()
50 | except ValueError:
51 | logger.warning("Error parsing JSON response body")
52 |
53 | if self.details:
54 | if "data" in self.details.keys():
55 | self.message = self.details["data"].get("message")
56 | elif "message" in self.details.keys():
57 | self.message = self.details["message"]
58 | else:
59 | self.message = None
60 | """The error message from the parsed API response."""
61 |
62 | super().__init__(
63 | "[{status_code}]{status} - {message}".format(
64 | status_code=self.status_code,
65 | status=" " + self.status if self.status else "",
66 | message=self.message or "Unknown Error",
67 | )
68 | )
69 |
70 | def __repr__(self):
71 | return "<{exception_name} [{status_code}]>".format(
72 | exception_name=self.__class__.__name__,
73 | status_code=self.status_code,
74 | )
75 |
76 |
77 | class MalformedResponse(LaceworkSDKException):
78 | """Raised when a malformed response is received from Lacework."""
79 |
80 | pass
81 |
82 |
83 | class RateLimitError(ApiError):
84 | """LAcework Rate-Limit exceeded Error.
85 |
86 | Raised when a rate-limit exceeded message is received and the request **will not** be retried.
87 | """
88 |
89 | pass
90 |
--------------------------------------------------------------------------------
/templates/.release_notes.md.j2:
--------------------------------------------------------------------------------
1 | ## Changes
2 |
3 | ## 🚀 Features
4 |
5 | {% for type_, commits in release["elements"] | dictsort %}
6 | {%- if type_ == "feature" %}
7 | {% for commit in commits %}
8 | * ({{type_}}):{{ commit.descriptions[0] }} by {{commit.commit.author.name}} in [`{{ commit.short_hash }}`]
9 | {%- endfor %}{% endif %}{% endfor %}
10 |
11 | ## 🐛 Bug Fixes
12 |
13 | {% for type_, commits in release["elements"] | dictsort %}
14 | {%- if type_ == "fix" %}
15 | {% for commit in commits %}
16 | * ({{type_}}):{{ commit.descriptions[0] }} by {{commit.commit.author.name}} in [`{{ commit.short_hash }}`]
17 | {%- endfor %}{% endif %}{% endfor %}
18 |
19 | ## 🧰 Maintenance
20 |
21 | {% for type_, commits in release["elements"] | dictsort %}
22 | {%- if type_ in ["chore", "ci", "documentation", "refactor", "test"] %}
23 | {% for commit in commits %}
24 | * ({{type_}}):{{ commit.descriptions[0] }} by {{commit.commit.author.name}} in [`{{ commit.short_hash }}`]
25 | {%- endfor %}{% endif %}{% endfor %}
26 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
--------------------------------------------------------------------------------
/tests/api/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import logging
7 | import os
8 | import random
9 | import string
10 |
11 | import laceworksdk
12 | import pytest
13 |
14 | from dotenv import load_dotenv
15 |
16 | logging.basicConfig(level=logging.DEBUG)
17 |
18 | load_dotenv()
19 |
20 |
21 | # Fixtures
22 |
23 | @pytest.fixture(scope="session")
24 | def api_key():
25 | return os.getenv("LW_API_KEY")
26 |
27 |
28 | @pytest.fixture(scope="session")
29 | def api_secret():
30 | return os.getenv("LW_API_SECRET")
31 |
32 |
33 | @pytest.fixture(scope="session")
34 | def account():
35 | return os.getenv("LW_ACCOUNT")
36 |
37 |
38 | @pytest.fixture(scope="session")
39 | def subaccount():
40 | return os.getenv("LW_SUBACCOUNT")
41 |
42 |
43 | @pytest.fixture(scope="session")
44 | def api_old(account, subaccount, api_key, api_secret):
45 | return laceworksdk.LaceworkClient(instance=account,
46 | subaccount=subaccount,
47 | api_key=api_key,
48 | api_secret=api_secret)
49 |
50 |
51 | @pytest.fixture(scope="session")
52 | def api(account, subaccount, api_key, api_secret):
53 | return laceworksdk.LaceworkClient(account=account,
54 | subaccount=subaccount,
55 | api_key=api_key,
56 | api_secret=api_secret)
57 |
58 |
59 | @pytest.fixture(scope="session")
60 | def api_env():
61 | return laceworksdk.LaceworkClient()
62 |
63 |
64 | @pytest.fixture(scope="session")
65 | def random_text():
66 | return "".join(random.choice(string.ascii_uppercase) for _ in range(8))
67 |
68 |
69 | @pytest.fixture(scope="session")
70 | def email_alert_channel_guid(api):
71 | response = api.alert_channels.search(
72 | json={
73 | "filters": [
74 | {
75 | "expression": "eq",
76 | "field": "type",
77 | "value": "EmailUser"
78 | }
79 | ],
80 | "returns": [
81 | "intgGuid"
82 | ]
83 | }
84 | )
85 | alert_channel_guid = response["data"][0]["intgGuid"]
86 | return alert_channel_guid
87 |
88 | @pytest.fixture(scope="session")
89 | def s3_alert_channel_guid(api):
90 | response = api.alert_channels.search(
91 | json={
92 | "filters": [
93 | {
94 | "expression": "eq",
95 | "field": "type",
96 | "value": "AwsS3"
97 | }
98 | ],
99 | "returns": [
100 | "intgGuid"
101 | ]
102 | }
103 | )
104 | alert_channel_guid = response["data"][0]["intgGuid"]
105 | return alert_channel_guid
106 |
107 |
108 | @pytest.fixture(scope="session")
109 | def aws_resource_group_guid(api):
110 | response = api.resource_groups.search(
111 | json={
112 | "filters": [
113 | {
114 | "expression": "eq",
115 | "field": "resourceType",
116 | "value": "AWS"
117 | }
118 | ],
119 | "returns": [
120 | "resourceGuid"
121 | ]
122 | }
123 | )
124 | resource_group_guid = response["data"][0]["resourceGuid"]
125 | return resource_group_guid
126 |
--------------------------------------------------------------------------------
/tests/api/test_base_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import random
7 |
8 | from datetime import datetime, timedelta, timezone
9 |
10 |
11 | class BaseEndpoint:
12 |
13 | OBJECT_ID_NAME = None
14 | OBJECT_TYPE = None
15 | OBJECT_PARAM_EXCEPTIONS = []
16 |
17 | def test_object_creation(self, api_object):
18 | assert isinstance(api_object, self.OBJECT_TYPE)
19 |
20 | def _check_object_values(self, param_dict, response):
21 | for key, value in param_dict.items():
22 | if key not in self.OBJECT_PARAM_EXCEPTIONS:
23 | key = self._convert_lower_camel_case(key)
24 |
25 | if isinstance(value, dict):
26 | assert value.items() <= response["data"][key].items()
27 | else:
28 | assert value == response["data"][key]
29 |
30 | def _get_object_classifier_test(self,
31 | api_object,
32 | classifier_name,
33 | classifier_key=None):
34 | if classifier_key is None:
35 | classifier_key = classifier_name
36 |
37 | classifier_value = self._get_random_object(api_object, classifier_key)
38 |
39 | if classifier_value:
40 | method = getattr(api_object, f"get_by_{classifier_name}")
41 |
42 | response = method(classifier_value)
43 |
44 | assert "data" in response.keys()
45 | if isinstance(response["data"], list):
46 | assert response["data"][0][classifier_key] == classifier_value
47 | elif isinstance(response["data"], dict):
48 | assert response["data"][classifier_key] == classifier_value
49 |
50 | def _get_random_object(self, api_object, key=None):
51 | response = api_object.get()
52 |
53 | if len(response["data"]) > 0:
54 | if key:
55 | return random.choice(response["data"])[key]
56 | else:
57 | return random.choice(response["data"])
58 | else:
59 | return None
60 |
61 | def _search_random_object(self, api_object, key=None, json=None):
62 | response = api_object.search(json=json)
63 |
64 | for page in response:
65 | if len(page["data"]) > 0:
66 | if key:
67 | return random.choice(page["data"])[key]
68 | else:
69 | return random.choice(page["data"])
70 |
71 | return None
72 |
73 | def _get_start_end_times(self, day_delta=1):
74 | current_time = datetime.now(timezone.utc)
75 | start_time = current_time - timedelta(days=day_delta)
76 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
77 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
78 |
79 | return start_time, end_time
80 |
81 | @staticmethod
82 | def _convert_lower_camel_case(param_name):
83 | words = param_name.split("_")
84 | first_word = words[0]
85 |
86 | if len(words) == 1:
87 | return first_word
88 |
89 | word_string = "".join([x.capitalize() or "_" for x in words[1:]])
90 |
91 | return f"{first_word}{word_string}"
92 |
--------------------------------------------------------------------------------
/tests/api/test_crud_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | from tests.api.test_base_endpoint import BaseEndpoint
7 | import pytest
8 |
9 |
10 | class CrudEndpoint(BaseEndpoint):
11 |
12 | def test_api_get(self, api_object):
13 | response = api_object.get()
14 |
15 | assert "data" in response.keys()
16 |
17 | def test_api_create(self, api_object, api_object_create_body, request):
18 | response = api_object.create(**api_object_create_body)
19 |
20 | assert "data" in response.keys()
21 | self._check_object_values(api_object_create_body, response)
22 |
23 | request.config.cache.set(self.OBJECT_ID_NAME, response["data"][self.OBJECT_ID_NAME])
24 |
25 | def test_api_search(self, api_object, request):
26 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
27 |
28 | if guid is None:
29 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
30 |
31 | assert guid is not None
32 | if guid:
33 | response = api_object.search(json={
34 | "filters": [
35 | {
36 | "expression": "eq",
37 | "field": self.OBJECT_ID_NAME,
38 | "value": guid
39 | }
40 | ],
41 | "returns": [
42 | self.OBJECT_ID_NAME
43 | ]
44 | })
45 |
46 | assert "data" in response.keys()
47 | assert len(response["data"]) == 1
48 | assert response["data"][0][self.OBJECT_ID_NAME] == guid
49 |
50 | def test_api_update(self, api_object, api_object_update_body, request):
51 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
52 |
53 | if guid is None:
54 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
55 |
56 | assert guid is not None
57 | if guid:
58 | response = api_object.update(guid, **api_object_update_body)
59 |
60 | assert "data" in response.keys()
61 |
62 | self._check_object_values(api_object_update_body, response)
63 |
64 | # if you don't run delete last you can run into some race conditions with the API
65 | @pytest.mark.order("last")
66 | def test_api_delete(self, api_object, request):
67 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
68 | assert guid is not None
69 | if guid:
70 | response = api_object.delete(guid)
71 | assert response.status_code == 204
72 |
--------------------------------------------------------------------------------
/tests/api/test_laceworksdk.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | from laceworksdk import LaceworkClient
7 |
8 |
9 | # Tests
10 |
11 | def test_lacework_client_api_object_creation(api):
12 | assert isinstance(api, LaceworkClient)
13 |
14 |
15 | def test_lacework_client_api_env_object_creation(api_env):
16 | assert isinstance(api_env, LaceworkClient)
17 |
18 |
19 | def test_lacework_client_api_set_org(api):
20 |
21 | api.set_org_level_access(True)
22 | assert api._session._org_level_access is True
23 |
24 | api.set_org_level_access(False)
25 | assert api._session._org_level_access is False
26 |
27 |
28 | def test_lacework_client_api_set_subaccount(api):
29 |
30 | old_subaccount = api._session._subaccount
31 |
32 | api.set_subaccount("testing")
33 | assert api._session._subaccount == "testing"
34 |
35 | api.set_subaccount(old_subaccount)
36 | assert api._session._subaccount == old_subaccount
37 |
--------------------------------------------------------------------------------
/tests/api/test_read_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import types
7 |
8 | from tests.api.test_base_endpoint import BaseEndpoint
9 |
10 |
11 | class ReadEndpoint(BaseEndpoint):
12 |
13 | OBJECT_TYPE = None
14 | OBJECT_MAP = {}
15 |
16 | def test_object_creation(self, api_object):
17 |
18 | if self.OBJECT_TYPE:
19 | assert isinstance(api_object, self.OBJECT_TYPE)
20 |
21 | if len(self.OBJECT_MAP) > 0:
22 | for attribute, object_type in self.OBJECT_MAP.items():
23 | assert isinstance(getattr(api_object, attribute), object_type)
24 |
25 | def test_api_get(self, api_object):
26 | if len(self.OBJECT_MAP) > 0:
27 | for attribute in self.OBJECT_MAP.keys():
28 | response = getattr(api_object, attribute).get()
29 | assert "data" in response.keys()
30 | else:
31 | response = api_object.get()
32 | assert "data" in response.keys()
33 |
34 | def test_api_search(self, api_object):
35 | random_object_id = self._get_random_object(api_object, self.OBJECT_ID_NAME)
36 | assert random_object_id is not None
37 | if random_object_id:
38 | response = api_object.search(json={
39 | "filters": [
40 | {
41 | "expression": "eq",
42 | "field": self.OBJECT_ID_NAME,
43 | "value": random_object_id
44 | }
45 | ],
46 | "returns": [
47 | self.OBJECT_ID_NAME
48 | ]
49 | })
50 |
51 | if isinstance(response, types.GeneratorType):
52 | response = next(response)
53 |
54 | assert "data" in response.keys()
55 | assert len(response["data"]) == 1
56 | assert response["data"][0][self.OBJECT_ID_NAME] == random_object_id
57 |
--------------------------------------------------------------------------------
/tests/api/test_search_endpoint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | from datetime import datetime, timedelta, timezone
7 |
8 |
9 | class SearchEndpoint:
10 |
11 | OBJECT_TYPE = None
12 | OBJECT_MAP = {}
13 |
14 | DAY_DELTA = 4
15 | MAX_PAGES = 2
16 |
17 | def test_object_creation(self, api_object):
18 |
19 | assert isinstance(api_object, self.OBJECT_TYPE)
20 |
21 | for attribute, object_type in self.OBJECT_MAP.items():
22 | assert isinstance(getattr(api_object, attribute), object_type)
23 |
24 | def test_api_search_by_date(self, api_object, filters=None):
25 | start_time, end_time = self._get_start_end_times(self.DAY_DELTA)
26 |
27 | json = {
28 | "timeFilters": {
29 | "startTime": start_time,
30 | "endTime": end_time
31 | }
32 | }
33 |
34 | if filters:
35 | json = {**json, **filters}
36 |
37 | if len(self.OBJECT_MAP) > 0:
38 | for attribute in self.OBJECT_MAP.keys():
39 | response = getattr(api_object, attribute).search(json=json)
40 | self._assert_pages(response, self.MAX_PAGES)
41 | else:
42 | response = api_object.search(json=json)
43 | self._assert_pages(response, self.MAX_PAGES)
44 |
45 | def _assert_pages(self, response, max_pages):
46 | page_count = 0
47 | for page in response:
48 | if page_count >= max_pages:
49 | return
50 | assert len(page["data"]) == page.get("paging", {}).get("rows", 0)
51 | page_count += 1
52 |
53 | def _get_start_end_times(self, day_delta):
54 | current_time = datetime.now(timezone.utc)
55 | start_time = current_time - timedelta(days=day_delta)
56 | start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
57 | end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
58 |
59 | return start_time, end_time
60 |
--------------------------------------------------------------------------------
/tests/api/v2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lacework/python-sdk/3b74b39cc26e5a8effffd8c09b02a17a04307a1f/tests/api/v2/__init__.py
--------------------------------------------------------------------------------
/tests/api/v2/test_activities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.activities import (
9 | ActivitiesAPI
10 | )
11 | from tests.api.test_search_endpoint import SearchEndpoint
12 |
13 | # Tests
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.activities
19 |
20 |
21 | class TestActivitiesEndpoint(SearchEndpoint):
22 |
23 | OBJECT_TYPE = ActivitiesAPI
24 | OBJECT_MAP = {
25 | "changed_files": ActivitiesAPI.ChangedFilesAPI,
26 | "dns": ActivitiesAPI.DnsAPI,
27 | "user_logins": ActivitiesAPI.UserLoginsAPI
28 | }
29 |
--------------------------------------------------------------------------------
/tests/api/v2/test_agent_access_tokens.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.agent_access_tokens import AgentAccessTokensAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.agent_access_tokens
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_update_body(random_text):
21 | return {
22 | "token_enabled": 1
23 | }
24 |
25 |
26 | class TestAgentAccessTokens(CrudEndpoint):
27 |
28 | OBJECT_ID_NAME = "accessToken"
29 | OBJECT_TYPE = AgentAccessTokensAPI
30 |
31 | def test_api_create(self):
32 | """
33 | Agent Access Tokens shouldn't be created with tests
34 | """
35 |
36 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
37 | @pytest.mark.order("first")
38 | def test_api_get_by_id(self, api_object):
39 | self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME)
40 |
41 | def test_api_delete(self):
42 | """
43 | Agent Access Tokens cannot currently be deleted
44 | """
45 |
--------------------------------------------------------------------------------
/tests/api/v2/test_agent_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.agent_info import (
9 | AgentInfoAPI
10 | )
11 | from tests.api.test_search_endpoint import SearchEndpoint
12 |
13 | # Tests
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.agent_info
19 |
20 |
21 | class TestAgentInfo(SearchEndpoint):
22 |
23 | OBJECT_TYPE = AgentInfoAPI
24 |
--------------------------------------------------------------------------------
/tests/api/v2/test_alert_channels.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.alert_channels import AlertChannelsAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.alert_channels
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_org(api):
21 | api.set_org_level_access(True)
22 | yield api.alert_channels
23 | api.set_org_level_access(False)
24 |
25 |
26 | @pytest.fixture(scope="module")
27 | def api_object_create_body(random_text):
28 | return {
29 | "name": f"Slack Test {random_text}",
30 | "type": "SlackChannel",
31 | "enabled": 1,
32 | "data": {
33 | "slackUrl": f"https://hooks.slack.com/services/TEST/WEBHOOK/{random_text}"
34 | }
35 | }
36 |
37 |
38 | @pytest.fixture(scope="module")
39 | def api_object_update_body(random_text):
40 | return {
41 | "name": f"Slack Test {random_text} Updated",
42 | "enabled": 0
43 | }
44 |
45 |
46 | class TestAlertChannels(CrudEndpoint):
47 |
48 | OBJECT_ID_NAME = "intgGuid"
49 | OBJECT_TYPE = AlertChannelsAPI
50 |
51 | @pytest.mark.order("first")
52 | def test_api_get_by_guid(self, api_object):
53 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
54 |
55 | @pytest.mark.order("second")
56 | def test_api_get_by_type(self, api_object):
57 | self._get_object_classifier_test(api_object, "type")
58 |
59 | def test_api_test(self, api_object):
60 | response = api_object.search(json={
61 | "filters": [
62 | {
63 | "expression": "ilike",
64 | "field": "name",
65 | "value": "default email"
66 | }
67 | ],
68 | "returns": [
69 | "intgGuid"
70 | ]
71 | })
72 |
73 | if len(response["data"]) > 0:
74 | default_email_guid = response["data"][0]["intgGuid"]
75 | response = api_object.test(guid=default_email_guid)
76 | assert response.status_code == 204
77 |
78 |
79 | @pytest.mark.parametrize("api_object", [pytest.lazy_fixture("api_object_org")])
80 | class TestAlertChannelsOrg(TestAlertChannels):
81 |
82 | @pytest.mark.flaky(reruns=10) # Because sometimes it tries to get deets for the test object that was just deleted
83 | # by the TestAlertChannels class
84 | @pytest.mark.order("first")
85 | def test_api_get_by_guid(self, api_object):
86 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
87 |
88 | @pytest.mark.order("second")
89 | def test_api_get_by_type(self, api_object):
90 | self._get_object_classifier_test(api_object, "type")
91 |
--------------------------------------------------------------------------------
/tests/api/v2/test_alert_profiles.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.alert_profiles import AlertProfilesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.alert_profiles
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "alert_profile_id": f"Test_{random_text}_AlertProfileID",
23 | "alerts": [
24 | {
25 | "name": f"HE_User_NewViolation_{random_text}",
26 | "eventName": f"Alert Event Name {random_text}",
27 | "description": f"Alert Event Description {random_text}",
28 | "subject": f"Alert Event Subject {random_text}"
29 | }
30 | ],
31 | "extends": "LW_HE_USERS_DEFAULT_PROFILE"
32 | }
33 |
34 |
35 | @pytest.fixture(scope="module")
36 | def api_object_update_body(random_text):
37 | return {
38 | "alerts": [
39 | {
40 | "name": f"HE_User_NewViolation_{random_text}_Updated",
41 | "eventName": f"Alert Event Name {random_text} Updated",
42 | "description": f"Alert Event Description {random_text} Updated",
43 | "subject": f"Alert Event Subject {random_text} Updated"
44 | }
45 | ]
46 | }
47 |
48 |
49 | class TestAlertProfiles(CrudEndpoint):
50 |
51 | OBJECT_ID_NAME = "alertProfileId"
52 | OBJECT_TYPE = AlertProfilesAPI
53 | OBJECT_PARAM_EXCEPTIONS = ["alerts"]
54 |
55 | def test_api_search(self):
56 | """
57 | Search is unavailable for this endpoint.
58 | """
59 | pass
60 |
61 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
62 | @pytest.mark.order("first")
63 | def test_api_get_by_id(self, api_object):
64 | self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME)
65 |
--------------------------------------------------------------------------------
/tests/api/v2/test_alert_rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.alert_rules import AlertRulesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.alert_rules
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text, email_alert_channel_guid):
21 | return {
22 | "type": "Event",
23 | "filters": {
24 | "name": f"Test Alert Rule {random_text}",
25 | "description": f"Test Alert Rule Description {random_text}",
26 | "enabled": 1,
27 | "severity": [1, 2, 3]
28 | },
29 | "intg_guid_list": [email_alert_channel_guid]
30 | }
31 |
32 |
33 | @pytest.fixture(scope="module")
34 | def api_object_update_body(random_text):
35 | return {
36 | "filters": {
37 | "name": f"Test Alert Rule {random_text} Updated",
38 | "enabled": 0
39 | }
40 | }
41 |
42 |
43 | class TestAlertRules(CrudEndpoint):
44 |
45 | OBJECT_ID_NAME = "mcGuid"
46 | OBJECT_TYPE = AlertRulesAPI
47 |
48 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
49 | @pytest.mark.order("first")
50 | def test_api_get_by_guid(self, api_object):
51 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
52 |
53 | # Ovveriding test due to API bug with "returns": ["mcGuid"]
54 | def test_api_search(self, api_object, request):
55 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
56 |
57 | if guid is None:
58 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
59 |
60 | assert guid is not None
61 | if guid:
62 | response = api_object.search(json={
63 | "filters": [
64 | {
65 | "expression": "eq",
66 | "field": self.OBJECT_ID_NAME,
67 | "value": guid
68 | }
69 | ],
70 | "returns": [
71 | "filters"
72 | ]
73 | })
74 |
--------------------------------------------------------------------------------
/tests/api/v2/test_alerts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from unittest import TestCase
9 | from urllib.error import HTTPError
10 | from laceworksdk.api.v2.alerts import AlertsAPI
11 | from tests.api.test_read_endpoint import ReadEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.alerts
19 |
20 |
21 | @pytest.fixture(scope="module")
22 | def open_alerts_filter():
23 | return {
24 | "filters": [
25 | {"field": "status", "expression": "eq", "value": "Open"}
26 | ]
27 | }
28 |
29 |
30 | class TestAlerts(ReadEndpoint):
31 |
32 | OBJECT_ID_NAME = "alertId"
33 | OBJECT_TYPE = AlertsAPI
34 |
35 | OBJECT_SCOPES = [
36 | "Details",
37 | "Investigation",
38 | "Events",
39 | "RelatedAlerts",
40 | "Integrations",
41 | # "Timeline" <-- Commented because most alertIds don't support this scope, causing test to fail
42 | ]
43 |
44 | def test_get_by_date(self, api_object):
45 | start_time, end_time = self._get_start_end_times()
46 | response = api_object.get(start_time=start_time, end_time=end_time)
47 | assert "data" in response.keys()
48 |
49 | def test_get_by_date_camelcase(self, api_object):
50 | start_time, end_time = self._get_start_end_times()
51 | response = api_object.get(startTime=start_time, endTime=end_time)
52 | assert "data" in response.keys()
53 |
54 | def test_get_duplicate_key(self, api_object):
55 | start_time, end_time = self._get_start_end_times()
56 | tester = TestCase()
57 | with tester.assertRaises(KeyError):
58 | api_object.get(start_time=start_time, startTime=start_time, endTime=end_time)
59 |
60 | @pytest.mark.flaky(reruns=10) # Because not all scopes are available for all alerts, causing it to fail randomly
61 | @pytest.mark.parametrize("scope", OBJECT_SCOPES)
62 | def test_get_details(self, api_object, scope):
63 |
64 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
65 | response = api_object.get_details(guid, scope)
66 | assert "data" in response.keys()
67 |
68 | def test_comment(self, api_object):
69 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
70 | response = api_object.comment(guid, "Test Comment")
71 | assert "data" in response.keys()
72 |
73 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to close an alert that isn't allowed
74 | def test_close_fp(self, api_object, open_alerts_filter):
75 | guid = self._search_random_object(api_object, self.OBJECT_ID_NAME, open_alerts_filter)
76 | if guid:
77 | response = api_object.close(guid, 1)
78 | assert "data" in response.keys()
79 |
80 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to close an alert that isn't allowed
81 | def test_close_other(self, api_object, open_alerts_filter):
82 | guid = self._search_random_object(api_object, self.OBJECT_ID_NAME, open_alerts_filter)
83 | if guid:
84 | response = api_object.close(guid, 0, "Test Reason")
85 | assert "data" in response.keys()
86 |
--------------------------------------------------------------------------------
/tests/api/v2/test_audit_logs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.audit_logs import AuditLogsAPI
9 | from tests.api.test_read_endpoint import ReadEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.audit_logs
17 |
18 |
19 | class TestAuditLogs(ReadEndpoint):
20 |
21 | OBJECT_TYPE = AuditLogsAPI
22 |
23 | def test_get_by_date(self, api_object):
24 | start_time, end_time = self._get_start_end_times()
25 | response = api_object.get(start_time=start_time, end_time=end_time)
26 | assert "data" in response.keys()
27 |
28 | def test_api_search(self, api_object):
29 | start_time, end_time = self._get_start_end_times()
30 | response = api_object.search(json={
31 | "timeFilter": {
32 | "startTime": start_time,
33 | "endTime": end_time
34 | },
35 | "filters": [
36 | {
37 | "expression": "rlike",
38 | "field": "userName",
39 | "value": "lacework.net"
40 | }
41 | ],
42 | "returns": [
43 | "accountName",
44 | "userAction",
45 | "userName"
46 | ]
47 | })
48 | assert "data" in response.keys()
49 |
--------------------------------------------------------------------------------
/tests/api/v2/test_cloud_accounts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.cloud_accounts import CloudAccountsAPI
9 | from tests.api.test_read_endpoint import ReadEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.cloud_accounts
17 |
18 |
19 | # TODO: Figure out a way to test creates/updates/deletes without breaking things
20 |
21 | # @pytest.fixture(scope="module")
22 | # def api_object_create_body(random_text):
23 | # return {
24 | # "name": f"AWS Config Test {random_text}",
25 | # "type": "AwsCfg",
26 | # "enabled": 1,
27 | # "data": {
28 | # "crossAccountCredentials": {
29 | # "externalId": f"{random_text}",
30 | # "roleArn": f"arn:aws:iam::434813966438:role/lacework-test-{random_text}"
31 | # }
32 | # }
33 | # }
34 |
35 |
36 | # @pytest.fixture(scope="module")
37 | # def api_object_update_body(random_text):
38 | # return {
39 | # "name": f"AWS Config Test {random_text} Updated",
40 | # "enabled": 0
41 | # }
42 |
43 |
44 | class TestCloudAccounts(ReadEndpoint):
45 |
46 | OBJECT_ID_NAME = "intgGuid"
47 | OBJECT_TYPE = CloudAccountsAPI
48 |
49 | @pytest.mark.order("first")
50 | def test_api_get_by_guid(self, api_object):
51 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
52 |
53 | @pytest.mark.order("second")
54 | def test_api_get_by_type(self, api_object):
55 | self._get_object_classifier_test(api_object, "type")
56 |
--------------------------------------------------------------------------------
/tests/api/v2/test_cloud_activities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from unittest import TestCase
9 |
10 | from laceworksdk.api.v2.cloud_activities import CloudActivitiesAPI
11 | from tests.api.test_read_endpoint import ReadEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.cloud_activities
19 |
20 |
21 | class TestCloudActivities(ReadEndpoint):
22 |
23 | OBJECT_ID_NAME = "eventId"
24 | OBJECT_TYPE = CloudActivitiesAPI
25 |
26 | def test_get_by_date(self, api_object):
27 | start_time, end_time = self._get_start_end_times()
28 | response = api_object.get(start_time=start_time, end_time=end_time)
29 | assert "data" in response.keys()
30 |
31 | def test_get_by_date_camelcase(self, api_object):
32 | start_time, end_time = self._get_start_end_times()
33 | response = api_object.get(startTime=start_time, endTime=end_time)
34 | assert "data" in response.keys()
35 |
36 | def test_get_duplicate_key(self, api_object):
37 | start_time, end_time = self._get_start_end_times()
38 | tester = TestCase()
39 | with tester.assertRaises(KeyError):
40 | api_object.get(start_time=start_time, startTime=start_time, endTime=end_time)
41 |
42 | def test_get_pages(self, api_object):
43 | response = api_object.get_pages()
44 |
45 | for page in response:
46 | assert "data" in page.keys()
47 |
48 | def test_get_data_items(self, api_object):
49 | start_time, end_time = self._get_start_end_times()
50 | response = api_object.get_data_items(start_time=start_time, end_time=end_time)
51 |
52 | event_keys = set([
53 | "endTime",
54 | "entityMap",
55 | "eventActor",
56 | "eventId",
57 | "eventModel",
58 | "eventType",
59 | "startTime"
60 | ])
61 |
62 | for item in response:
63 | assert event_keys.issubset(item.keys())
64 |
--------------------------------------------------------------------------------
/tests/api/v2/test_configs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.configs import (
9 | ConfigsAPI,
10 | )
11 | from tests.api.test_read_endpoint import ReadEndpoint
12 | from tests.api.test_search_endpoint import SearchEndpoint
13 |
14 | # Tests
15 |
16 |
17 | @pytest.fixture(scope="module")
18 | def api_object(api):
19 | return api.configs
20 |
21 |
22 | class TestConfigsEndpoint(SearchEndpoint):
23 |
24 | OBJECT_TYPE = ConfigsAPI
25 | OBJECT_MAP = {
26 | "compliance_evaluations": ConfigsAPI.ComplianceEvaluationsAPI,
27 | }
28 |
29 | @pytest.mark.parametrize("dataset", ["AwsCompliance", "AzureCompliance", "GcpCompliance"])
30 | def test_api_search_by_date(self, api_object, dataset):
31 |
32 | return super().test_api_search_by_date(api_object=api_object, filters={"dataset": dataset})
33 |
34 |
35 | class TestConfigsLookups(ReadEndpoint):
36 |
37 | OBJECT_TYPE = ConfigsAPI
38 | OBJECT_MAP = {
39 | "azure_subscriptions": ConfigsAPI.AzureSubscriptions,
40 | "gcp_projects": ConfigsAPI.GcpProjects
41 | }
42 |
43 | def test_api_search(self, api_object):
44 | pass
45 |
--------------------------------------------------------------------------------
/tests/api/v2/test_container_registries.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.container_registries import ContainerRegistriesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.container_registries
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "name": f"Docker Hub Test {random_text}",
23 | "type": "ContVulnCfg",
24 | "enabled": 1,
25 | "data": {
26 | "registryType": "INLINE_SCANNER"
27 | }
28 | }
29 |
30 |
31 | @pytest.fixture(scope="module")
32 | def api_object_update_body(random_text):
33 | return {
34 | "name": f"Docker Hub Test {random_text} Updated",
35 | "enabled": 0
36 | }
37 |
38 |
39 | class TestContainerRegistries(CrudEndpoint):
40 |
41 | OBJECT_ID_NAME = "intgGuid"
42 | OBJECT_TYPE = ContainerRegistriesAPI
43 |
44 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
45 | @pytest.mark.order("first")
46 | def test_api_get_by_guid(self, api_object):
47 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
48 |
49 | @pytest.mark.order("second")
50 | def test_api_get_by_type(self, api_object):
51 | self._get_object_classifier_test(api_object, "type")
52 |
--------------------------------------------------------------------------------
/tests/api/v2/test_contract_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.contract_info import ContractInfoAPI
9 | from tests.api.test_base_endpoint import BaseEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.contract_info
17 |
18 |
19 | class TestContractInfo(BaseEndpoint):
20 |
21 | OBJECT_ID_NAME = "alertId"
22 | OBJECT_TYPE = ContractInfoAPI
23 |
24 | @pytest.mark.quarantine_test # Bug in API as of 1/25/24 RAIN-50089
25 | def test_api_get(self, api_object):
26 | response = api_object.get()
27 | assert "data" in response.keys()
28 |
--------------------------------------------------------------------------------
/tests/api/v2/test_data_export_rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.data_export_rules import DataExportRulesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.data_export_rules
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text, s3_alert_channel_guid):
21 | return {
22 | "type": "Dataexport",
23 | "filters": {
24 | "name": f"Test Data Export Rule {random_text}",
25 | "description": f"Test Data Export Rule Description {random_text}",
26 | "enabled": 1
27 | },
28 | "intg_guid_list": [s3_alert_channel_guid]
29 | }
30 |
31 |
32 | @pytest.fixture(scope="module")
33 | def api_object_update_body(random_text):
34 | return {
35 | "filters": {
36 | "name": f"Test Data Export Rule {random_text} (Updated)",
37 | "enabled": False
38 | }
39 | }
40 |
41 |
42 | class TestDataExportRules(CrudEndpoint):
43 |
44 | OBJECT_ID_NAME = "mcGuid"
45 | OBJECT_TYPE = DataExportRulesAPI
46 |
47 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
48 | @pytest.mark.order("first")
49 | def test_api_get_by_guid(self, api_object):
50 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
51 |
--------------------------------------------------------------------------------
/tests/api/v2/test_datasources.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.datasources import DatasourcesAPI
9 | from tests.api.test_base_endpoint import BaseEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.datasources
17 |
18 |
19 | class TestDatasources(BaseEndpoint):
20 |
21 | OBJECT_ID_NAME = "name"
22 | OBJECT_TYPE = DatasourcesAPI
23 |
24 | def test_api_get(self, api_object):
25 | response = api_object.get()
26 | assert "data" in response.keys()
27 |
28 | def test_get_datasource(self, api_object):
29 | response = api_object.get_datasource("LW_CFG_AZURE_NETWORK_APPLICATIONGATEWAYS")
30 | assert "data" in response.keys()
31 |
32 | def test_list_data_sources(self, api_object):
33 | response = api_object.list_data_sources()
34 | assert isinstance(response[0], tuple)
--------------------------------------------------------------------------------
/tests/api/v2/test_entities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.entities import (
9 | EntitiesAPI,
10 | )
11 | from tests.api.test_search_endpoint import SearchEndpoint
12 |
13 | # Tests
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.entities
19 |
20 |
21 | class TestEntitiesEndpoint(SearchEndpoint):
22 |
23 | OBJECT_TYPE = EntitiesAPI
24 | OBJECT_MAP = {
25 | "applications": EntitiesAPI.ApplicationsAPI,
26 | "command_lines": EntitiesAPI.CommandLinesAPI,
27 | "containers": EntitiesAPI.ContainersAPI,
28 | "files": EntitiesAPI.FilesAPI,
29 | "images": EntitiesAPI.ImagesAPI,
30 | "internal_ip_addresses": EntitiesAPI.InternalIPAddressesAPI,
31 | "k8s_pods": EntitiesAPI.K8sPodsAPI,
32 | "machines": EntitiesAPI.MachinesAPI,
33 | "machine_details": EntitiesAPI.MachineDetailsAPI,
34 | "network_interfaces": EntitiesAPI.NetworkInterfacesAPI,
35 | "new_file_hashes": EntitiesAPI.NewFileHashesAPI,
36 | "packages": EntitiesAPI.PackagesAPI,
37 | "processes": EntitiesAPI.ProcessesAPI,
38 | "users": EntitiesAPI.UsersAPI
39 | }
40 |
--------------------------------------------------------------------------------
/tests/api/v2/test_events.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.events import EventsAPI
9 | from tests.api.test_search_endpoint import SearchEndpoint
10 |
11 | # Tests
12 |
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.events
17 |
18 |
19 | class TestEventsEndpoint(SearchEndpoint):
20 |
21 | OBJECT_TYPE = EventsAPI
22 |
--------------------------------------------------------------------------------
/tests/api/v2/test_inventory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.inventory import (
9 | InventoryAPI
10 | )
11 | from tests.api.test_search_endpoint import SearchEndpoint
12 |
13 | # Tests
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.inventory
19 |
20 |
21 | class TestInventory(SearchEndpoint):
22 |
23 | OBJECT_TYPE = InventoryAPI
24 |
25 | @pytest.mark.parametrize("csp", ["AWS", "Azure", "GCP"])
26 | def test_api_search_by_date(self, api_object, csp):
27 |
28 | return super().test_api_search_by_date(api_object=api_object, filters={"csp": csp})
29 |
30 | @pytest.mark.parametrize("dataset", ["AwsCompliance", "GcpCompliance"])
31 | def test_api_search_by_date_deprecated(self, api_object, dataset):
32 |
33 | return super().test_api_search_by_date(api_object=api_object, filters={"dataset": dataset})
34 |
35 | def test_inventory_scan(self, api_object, request):
36 | response = api_object.scan(csp="AWS")
37 |
38 | assert "data" in response.keys()
39 |
40 | def test_inventory_status(self, api_object, request):
41 | response = api_object.scan(csp="AWS")
42 |
43 | assert "data" in response.keys()
44 |
--------------------------------------------------------------------------------
/tests/api/v2/test_organization_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.organization_info import OrganizationInfoAPI
9 | from tests.api.test_base_endpoint import BaseEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.organization_info
17 |
18 |
19 | class TestOrganizationInfo(BaseEndpoint):
20 |
21 | OBJECT_ID_NAME = "name"
22 | OBJECT_TYPE = OrganizationInfoAPI
23 |
24 | def test_api_get(self, api_object):
25 | response = api_object.get()
26 | keys = set([
27 | "orgAccount",
28 | "orgAccountUrl"
29 | ])
30 |
31 | for item in response["data"]:
32 | assert keys.issubset(item.keys())
33 |
--------------------------------------------------------------------------------
/tests/api/v2/test_policies.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import random
7 |
8 | import pytest
9 |
10 | from laceworksdk.api.v2.policies import PoliciesAPI
11 | from tests.api.test_crud_endpoint import CrudEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.policies
19 |
20 |
21 | @pytest.fixture(scope="module")
22 | def api_object_create_body(random_text, query):
23 | return {
24 | "policy_type": "Violation",
25 | "query_id": query["queryId"],
26 | "enabled": True,
27 | "title": random_text,
28 | "description": f"{random_text} description",
29 | "remediation": "Policy remediation",
30 | "severity": "high",
31 | "alert_enabled": True,
32 | "alert_profile": "LW_CloudTrail_Alerts"
33 | }
34 |
35 |
36 | @pytest.fixture(scope="module")
37 | def api_object_update_body():
38 | return {
39 | "enabled": False
40 | }
41 |
42 |
43 | @pytest.fixture(scope="module")
44 | def api_object_bulk_update_body():
45 | return [
46 | {
47 | "policyId": "lacework-global-24",
48 | "enabled": True,
49 | "severity": "medium"
50 | },
51 | {
52 | "policyId": "lacework-global-218",
53 | "enabled": True
54 | }
55 | ]
56 |
57 |
58 | @pytest.fixture(scope="module")
59 | def query(api):
60 | queries = api.queries.get()
61 | queries = list(filter(lambda elem: elem["owner"] == "Lacework" and "LW_Global_AWS_CTA" in elem["queryId"], queries["data"]))
62 | query = random.choice(queries)
63 | return query
64 |
65 |
66 | class TestPolicies(CrudEndpoint):
67 |
68 | OBJECT_ID_NAME = "policyId"
69 | OBJECT_TYPE = PoliciesAPI
70 |
71 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
72 | def test_api_get_by_id(self, api_object):
73 | self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME)
74 |
75 | def test_api_bulk_update(self, api_object, api_object_bulk_update_body):
76 | response = api_object.bulk_update(api_object_bulk_update_body)
77 | assert "data" in response.keys()
78 |
79 | def test_api_search(self):
80 | pass
81 |
--------------------------------------------------------------------------------
/tests/api/v2/test_policy_exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 | import random
8 | from laceworksdk.api.v2.policy_exceptions import PolicyExceptionsAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.policy_exceptions
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def policy_id():
21 | return "lacework-global-100"
22 |
23 |
24 | @pytest.fixture(scope="module")
25 | def policy_exception_id(api_object, policy_id):
26 | response = api_object.get(policy_id=policy_id)
27 | return random.choice(response["data"])['exceptionId']
28 |
29 |
30 | @pytest.fixture(scope="module")
31 | def api_object_create_body(random_text):
32 | return {
33 | "description": f"Test Policy Exception {random_text}",
34 | "constraints": [
35 | {
36 | "fieldKey": "resourceTags",
37 | "fieldValues": [
38 | {"key": "TestKey", "value": "TestValue"}
39 | ]
40 | }
41 | ]
42 | }
43 |
44 |
45 | @pytest.fixture(scope="module")
46 | def api_object_update_body(random_text):
47 | return {
48 | "description": f"Test Policy Exception {random_text} (Updated)",
49 | "constraints": [
50 | {
51 | "fieldKey": "resourceTags",
52 | "fieldValues": [
53 | {"key": "TestKey", "value": "UpdatedTestValue"}
54 | ]
55 | }
56 | ]
57 | }
58 |
59 |
60 | class TestPolicyExceptions(CrudEndpoint):
61 |
62 | OBJECT_ID_NAME = "exceptionId"
63 | OBJECT_TYPE = PolicyExceptionsAPI
64 |
65 | def test_api_get(self, api_object, policy_id):
66 | response = api_object.get(policy_id=policy_id)
67 |
68 | assert "data" in response.keys()
69 |
70 | def test_api_create(self, api_object, policy_id, api_object_create_body, request):
71 | response = api_object.create(policy_id, **api_object_create_body)
72 |
73 | assert "data" in response.keys()
74 | self._check_object_values(api_object_create_body, response)
75 |
76 | request.config.cache.set(self.OBJECT_ID_NAME, response["data"][self.OBJECT_ID_NAME])
77 |
78 | @pytest.mark.quarantine_test # Stopped working around 2/27/2024. API bug or change?
79 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
80 | @pytest.mark.order("first")
81 | def test_api_get_by_guid(self, api_object, policy_id, policy_exception_id):
82 | response = api_object.get(policy_exception_id, policy_id)
83 | assert "data" in response.keys()
84 |
85 | def test_api_search(self):
86 | pass
87 |
88 | @pytest.mark.quarantine_test # Stopped working around 2/27/2024. API bug or change?
89 | def test_api_update(self, api_object, policy_id, api_object_update_body, request):
90 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
91 |
92 | if guid is None:
93 | guid = self._get_random_object(api_object, self.OBJECT_ID_NAME)
94 |
95 | assert guid is not None
96 | if guid:
97 | response = api_object.update(guid, policy_id, **api_object_update_body)
98 |
99 | assert "data" in response.keys()
100 |
101 | self._check_object_values(api_object_update_body, response)
102 |
103 | @pytest.mark.quarantine_test # Stopped working around 2/27/2024. API bug or change?
104 | def test_api_delete(self, api_object, policy_id, request):
105 | guid = request.config.cache.get(self.OBJECT_ID_NAME, None)
106 | assert guid is not None
107 | if guid:
108 | response = api_object.delete(guid, policy_id)
109 | assert response.status_code == 204
110 |
--------------------------------------------------------------------------------
/tests/api/v2/test_queries.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import random
7 |
8 | import pytest
9 |
10 | from laceworksdk.api.v2.queries import QueriesAPI
11 | from tests.api.test_crud_endpoint import CrudEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.queries
19 |
20 |
21 | @pytest.fixture(scope="module")
22 | def api_object_create_body(random_text):
23 | return {
24 | "query_id": random_text,
25 | "query_text": f"""{random_text} {{
26 | source {{CloudTrailRawEvents e}}
27 | filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{random_text}'}}
28 | return distinct {{EVENT_NAME, EVENT}}
29 | }}"""
30 | }
31 |
32 |
33 | @pytest.fixture(scope="module")
34 | def api_object_update_body(random_text):
35 | return {
36 | "query_text": f"""{random_text} {{
37 | source {{CloudTrailRawEvents e}}
38 | filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{random_text}_updated'}}
39 | return distinct {{EVENT_NAME, EVENT}}
40 | }}"""
41 | }
42 |
43 |
44 | @pytest.fixture(scope="module")
45 | def query(api):
46 | queries = api.queries.get()
47 | queries = list(filter(lambda elem: elem["owner"] == "Lacework" and "LW_Global_AWS_CTA" in elem["queryId"], queries["data"]))
48 | query = random.choice(queries)
49 | return query
50 |
51 |
52 | class TestQueries(CrudEndpoint):
53 |
54 | OBJECT_ID_NAME = "queryId"
55 | OBJECT_TYPE = QueriesAPI
56 |
57 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
58 | @pytest.mark.order("first")
59 | def test_api_get_by_id(self, api_object):
60 | self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME)
61 |
62 | def test_queries_api_execute_by_id(self, api_object, query):
63 | start_time, end_time = self._get_start_end_times()
64 | response = api_object.execute_by_id(
65 | query_id=query["queryId"],
66 | arguments={
67 | "StartTimeRange": start_time,
68 | "EndTimeRange": end_time,
69 | }
70 | )
71 | assert "data" in response.keys()
72 |
73 | def test_queries_api_validate(self, api_object, query):
74 | response = api_object.validate(query_text=query["queryText"])
75 | assert "data" in response.keys()
76 |
77 | def test_api_search(self):
78 | pass
79 |
--------------------------------------------------------------------------------
/tests/api/v2/test_report_definitions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import random
7 |
8 | import pytest
9 |
10 | from laceworksdk.api.v2.report_definitions import ReportDefinitionsAPI
11 | from tests.api.test_crud_endpoint import CrudEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.report_definitions
19 |
20 |
21 | @pytest.fixture(scope="module")
22 | def aws_account(api):
23 | cloud_accounts = api.cloud_accounts.get_by_type("AwsCfg")
24 |
25 | if len(cloud_accounts["data"]):
26 | aws_role = random.choice(cloud_accounts["data"])["data"]["crossAccountCredentials"]["roleArn"]
27 | aws_account = aws_role.split(":")[4]
28 | return aws_account
29 |
30 |
31 | @pytest.fixture(scope="module")
32 | def api_object_create_body(random_text, aws_resource_group_guid, email_alert_channel_guid):
33 | return {
34 | "report_name": f"Test_{random_text}_Report",
35 | "report_type": "COMPLIANCE",
36 | "sub_report_type": "AWS",
37 | "report_definition": {
38 | "sections": [
39 | {
40 | "category": "1",
41 | "title": "Critical policies collection",
42 | "policies": [
43 | "lacework-global-31"
44 | ]
45 | }
46 | ]
47 | }
48 | }
49 |
50 |
51 | @pytest.fixture(scope="module")
52 | def api_object_update_body(random_text):
53 | return {
54 | "report_name": f"Test_{random_text}_Report",
55 | "report_definition": {
56 | "sections": [
57 | {
58 | "category": "1",
59 | "title": "Critical policies collection",
60 | "policies": [
61 | "lacework-global-31"
62 | ]
63 | }
64 | ]
65 | }
66 | }
67 |
68 |
69 | class TestReportDefinitions(CrudEndpoint):
70 |
71 | OBJECT_ID_NAME = "reportDefinitionGuid"
72 | OBJECT_TYPE = ReportDefinitionsAPI
73 | OBJECT_PARAM_EXCEPTIONS = ["alerts"]
74 |
75 | def test_api_search(self):
76 | """
77 | Search is unavailable for this endpoint.
78 | """
79 | pass
80 |
81 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
82 | @pytest.mark.order("first")
83 | def test_api_get_by_id(self, api_object):
84 | self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME)
85 |
--------------------------------------------------------------------------------
/tests/api/v2/test_report_rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.report_rules import ReportRulesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.report_rules
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text, email_alert_channel_guid, aws_resource_group_guid):
21 | return {
22 | "type": "Report",
23 | "filters": {
24 | "name": f"Test Report Rule {random_text}",
25 | "description": f"Test Report Rule Description {random_text}",
26 | "enabled": 1,
27 | "resourceGroups": [aws_resource_group_guid],
28 | "severity": [1, 2, 3]
29 | },
30 | "intg_guid_list": [email_alert_channel_guid],
31 | "report_notification_types": {
32 | "awsComplianceEvents": True,
33 | "awsCisS3": True
34 | }
35 | }
36 |
37 |
38 | @pytest.fixture(scope="module")
39 | def api_object_update_body(random_text):
40 | return {
41 | "filters": {
42 | "name": f"Test Report Rule {random_text} (Updated)",
43 | "enabled": False
44 | }
45 | }
46 |
47 |
48 | class TestReportRules(CrudEndpoint):
49 |
50 | OBJECT_ID_NAME = "mcGuid"
51 | OBJECT_TYPE = ReportRulesAPI
52 |
53 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
54 | @pytest.mark.order("first")
55 | def test_api_get_by_guid(self, api_object):
56 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
57 |
--------------------------------------------------------------------------------
/tests/api/v2/test_reports.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import random
7 |
8 | import pytest
9 |
10 | from laceworksdk.api.v2.reports import ReportsAPI
11 | from tests.api.test_base_endpoint import BaseEndpoint
12 |
13 |
14 | # Tests
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.reports
19 |
20 |
21 | @pytest.fixture(scope="module")
22 | def aws_account(api):
23 | cloud_accounts = api.cloud_accounts.get_by_type("AwsCfg")
24 |
25 | if len(cloud_accounts["data"]):
26 | aws_role = random.choice(cloud_accounts["data"])["data"]["crossAccountCredentials"]["roleArn"]
27 | aws_account = aws_role.split(":")[4]
28 | return aws_account
29 |
30 |
31 | class TestReports(BaseEndpoint):
32 |
33 | OBJECT_TYPE = ReportsAPI
34 |
35 | def test_api_get_aws_soc2_json(self, api_object, aws_account):
36 | if aws_account:
37 | response = api_object.get(
38 | primary_query_id=aws_account,
39 | format="json",
40 | report_type="AWS_SOC_Rev2",
41 | )
42 | assert "data" in response.keys()
43 |
44 | def test_api_get_aws_cis14_html(self, api_object, aws_account):
45 | if aws_account:
46 | response = api_object.get(
47 | primary_query_id=aws_account,
48 | format="html",
49 | report_type="AWS_CIS_14",
50 | )
51 | assert "".encode("utf-8") in response
52 |
--------------------------------------------------------------------------------
/tests/api/v2/test_resource_groups.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 | from laceworksdk.api.v2.resource_groups import ResourceGroupsAPI
8 | from tests.api.test_crud_endpoint import CrudEndpoint
9 |
10 |
11 | # Tests
12 |
13 | @pytest.fixture(scope="module")
14 | def api_object(api):
15 | return api.resource_groups
16 |
17 |
18 | @pytest.fixture(scope="module")
19 | def api_object_create_body(random_text):
20 | return {
21 | "resource_name": f"AWS Test {random_text}",
22 | "resource_type": "AWS",
23 | "enabled": True,
24 | "props": {
25 | "description": f"Test Description {random_text}",
26 | "accountIds": ["123456789012"]
27 | }
28 | }
29 |
30 |
31 | @pytest.fixture(scope="module")
32 | def api_object_update_body(random_text):
33 | return {
34 | "resource_name": f"AWS Test {random_text} (Updated)",
35 | "enabled": 0,
36 | "props": {
37 | "description": f"Test Description {random_text} (Updated)",
38 | "accountIds": ["123456789012"]
39 | }
40 | }
41 |
42 |
43 | class TestResourceGroups(CrudEndpoint):
44 |
45 | OBJECT_ID_NAME = "resourceGuid"
46 | OBJECT_TYPE = ResourceGroupsAPI
47 | OBJECT_PARAM_EXCEPTIONS = ["props"]
48 |
49 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
50 | @pytest.mark.order("first")
51 | def test_api_get_by_guid(self, api_object):
52 | response = api_object.get()
53 | guid = None
54 | if len(response["data"]) > 0:
55 | for entry in response['data']:
56 | if self.OBJECT_ID_NAME in entry:
57 | guid = entry[self.OBJECT_ID_NAME]
58 | if guid:
59 | response = api_object.get_by_guid(guid)
60 | assert "data" in response.keys()
61 | assert response["data"]['resourceGuid'] == guid
62 |
--------------------------------------------------------------------------------
/tests/api/v2/test_schemas.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.schemas import SchemasAPI
9 | from tests.api.test_base_endpoint import BaseEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.schemas
17 |
18 |
19 | class TestSchemas(BaseEndpoint):
20 |
21 | OBJECT_ID_NAME = "name"
22 | OBJECT_TYPE = SchemasAPI
23 |
24 | def test_schemas_api_get(self, api_object):
25 | response = api_object.get()
26 | assert len(response) > 0
27 |
28 | def test_schemas_api_get_type_schema(self, api_object):
29 | response = api_object.get()
30 |
31 | exempt_schemas = [
32 | "Activities",
33 | "Configs",
34 | "Entities",
35 | "Vulnerabilities"
36 | ]
37 |
38 | for schema_type in response:
39 | if schema_type in exempt_schemas:
40 | continue
41 |
42 | response = api_object.get(type=schema_type)
43 |
44 | if type(response) is dict:
45 | if len(response) > 0:
46 | if "oneOf" in response.keys():
47 | for schema in response["oneOf"]:
48 | assert "properties" in schema.keys()
49 | else:
50 | assert "properties" in response.keys()
51 | else:
52 | assert True
53 | elif type(response) is list:
54 | assert True
55 | else:
56 | assert False
57 |
58 | def test_schemas_api_get_subtype_schema(self, api_object):
59 | type = "AlertChannels"
60 | response = api_object.get(type=type)
61 |
62 | for subtype_schema in response["oneOf"]:
63 |
64 | subtype = subtype_schema["properties"]["type"]["enum"][0]
65 |
66 | response = api_object.get_by_subtype(type=type, subtype=subtype)
67 | assert "properties" in response.keys()
68 |
--------------------------------------------------------------------------------
/tests/api/v2/test_team_members.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.team_members import TeamMembersAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.team_members
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "user_name": f"{random_text.lower()}@lacework.net",
23 | "props": {
24 | "firstName": "John",
25 | "lastName": "Doe",
26 | "company": "Lacework",
27 | "accountAdmin": True
28 | },
29 | "user_enabled": True
30 | }
31 |
32 |
33 | @pytest.fixture(scope="module")
34 | def api_object_update_body():
35 | return {
36 | "user_enabled": False
37 | }
38 |
39 |
40 | class TestTeamMembers(CrudEndpoint):
41 |
42 | OBJECT_ID_NAME = "userGuid"
43 | OBJECT_TYPE = TeamMembersAPI
44 |
45 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
46 | @pytest.mark.order("first")
47 | def test_api_get_by_guid(self, api_object):
48 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
49 |
--------------------------------------------------------------------------------
/tests/api/v2/test_team_users.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.team_users import TeamUsersAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.team_users
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "name": "John Doe",
23 | "email": f"{random_text.lower()}@lacework.net",
24 | "company": "Lacework",
25 | }
26 |
27 |
28 | @pytest.fixture(scope="module")
29 | def api_object_update_body():
30 | return {
31 | "user_enabled": 0
32 | }
33 |
34 |
35 | class TestTeamUsers(CrudEndpoint):
36 |
37 | OBJECT_ID_NAME = "userGuid"
38 | OBJECT_TYPE = TeamUsersAPI
39 |
40 | def test_api_search(self, api_object, request):
41 | "Not implemented"
42 | pass
43 |
44 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
45 | @pytest.mark.order("first")
46 | def test_api_get_by_guid(self, api_object):
47 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
48 |
--------------------------------------------------------------------------------
/tests/api/v2/test_user_groups.py:
--------------------------------------------------------------------------------
1 |
2 | # -*- coding: utf-8 -*-
3 | """
4 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
5 | """
6 |
7 | import pytest
8 |
9 | from laceworksdk.api.v2.user_groups import UserGroupsAPI
10 | from laceworksdk.exceptions import ApiError
11 | from tests.api.test_base_endpoint import BaseEndpoint
12 | from laceworksdk import LaceworkClient
13 |
14 |
15 | # Tests
16 |
17 | @pytest.fixture(scope="module")
18 | def api_object(api):
19 | return api.user_groups
20 |
21 | @pytest.fixture(scope="module")
22 | def test_user(api, random_text):
23 | res = api.team_users.create(f"test{random_text}", f"noreply+{random_text}@lacework.com", "test")
24 | guid = res["data"]["userGuid"]
25 | yield guid
26 | api.team_users.delete(guid)
27 |
28 |
29 | class TestUserGroups(BaseEndpoint):
30 |
31 | OBJECT_TYPE = UserGroupsAPI
32 |
33 | def test_add_user(self, api_object, test_user):
34 | response = api_object.add_users("LACEWORK_USER_GROUP_POWER_USER", [test_user])
35 | assert "data" in response.keys()
36 |
37 | def test_remove_user(self, api_object, test_user):
38 | response = api_object.remove_users("LACEWORK_USER_GROUP_POWER_USER", [test_user])
39 | assert "data" in response.keys()
40 |
41 | def test_add_user_should_fail_with_invalid_data(self, api_object):
42 | with pytest.raises(ApiError) as e:
43 | api_object.add_users("LACEWORK_USER_GROUP_POWER_USER", ["fake"])
44 | assert "400" in str(e.value)
45 |
46 | def test_remove_user_should_fail_with_invalid_data(self, api_object):
47 | with pytest.raises(ApiError) as e:
48 | api_object.remove_users("LACEWORK_USER_GROUP_POWER_USER", ["fake"])
49 | assert "400" in str(e.value)
50 |
--------------------------------------------------------------------------------
/tests/api/v2/test_user_profile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.user_profile import UserProfileAPI
9 | from tests.api.test_base_endpoint import BaseEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.user_profile
17 |
18 |
19 | class TestUserProfile(BaseEndpoint):
20 |
21 | OBJECT_TYPE = UserProfileAPI
22 |
23 | def test_api_get(self, api_object):
24 | response = api_object.get()
25 | keys = set([
26 | "username",
27 | "orgAccount",
28 | "url",
29 | "orgAdmin",
30 | "orgUser",
31 | "accounts"
32 | ])
33 |
34 | for item in response["data"]:
35 | assert keys.issubset(item.keys())
36 |
37 | def test_api_get_subaccount(self, api_object):
38 | response = api_object.get(account_name="tech-ally")
39 | assert "data" in response
40 |
--------------------------------------------------------------------------------
/tests/api/v2/test_vulnerabilities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.vulnerabilities import (
9 | VulnerabilitiesAPI
10 | )
11 | from tests.api.test_search_endpoint import SearchEndpoint
12 |
13 | # Tests
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def api_object(api):
18 | return api.vulnerabilities
19 |
20 |
21 | class TestVulnerabilitesEndpoint(SearchEndpoint):
22 |
23 | OBJECT_TYPE = VulnerabilitiesAPI
24 | OBJECT_MAP = {
25 | "containers": VulnerabilitiesAPI.ContainerVulnerabilitiesAPI,
26 | "hosts": VulnerabilitiesAPI.HostVulnerabilitiesAPI,
27 | "imageSummary": VulnerabilitiesAPI.ImageSummaryVulnerabilitiesAPI
28 | }
29 |
30 | def test_vulnerabilities_containers_api_scan(self, api_object, request):
31 | response = api_object.containers.scan("index.docker.io",
32 | "alannix/vulnerable-struts",
33 | "latest")
34 |
35 | assert "data" in response.keys()
36 | if isinstance(response["data"], list):
37 | scan_request_id = response["data"][0].get("requestId")
38 | elif isinstance(response["data"], dict):
39 | scan_request_id = response["data"].get("requestId")
40 |
41 | request.config.cache.set("scan_request_id", scan_request_id)
42 |
43 | def test_vulnerabilities_containers_api_scan_status(self, api_object, request):
44 | scan_request_id = request.config.cache.get("scan_request_id", None)
45 | assert scan_request_id is not None
46 | if scan_request_id:
47 | response = api_object.containers.status(request_id=scan_request_id)
48 | assert "data" in response.keys()
49 |
50 | if isinstance(response["data"], list):
51 | assert "status" in response["data"][0].keys()
52 | elif isinstance(response["data"], dict):
53 | assert "status" in response["data"].keys()
54 |
55 | def test_vulnerabilities_packages_api_object_creation(api, api_object):
56 | assert isinstance(api_object.packages, VulnerabilitiesAPI.SoftwarePackagesAPI)
57 |
58 | def test_vulnerabilities_packages_api_scan(api, api_object):
59 | response = api_object.packages.scan(os_pkg_info_list=[{
60 | "os": "Ubuntu",
61 | "osVer": "18.04",
62 | "pkg": "openssl",
63 | "pkgVer": "1.1.1-1ubuntu2.1~18.04.5"
64 | }, {
65 | "os": "Ubuntu",
66 | "osVer": "20.04",
67 | "pkg": "openssl",
68 | "pkgVer": "1.1.1-1ubuntu2.1~20.04"
69 | }])
70 | assert "data" in response.keys()
71 |
72 | def test_vulnerabilities_image_summary_search(api, api_object):
73 | json = {
74 | "timeFilter": {
75 | "startTime": "2024-03-18T00:00:00Z",
76 | "endTime": "2024-03-19T08:00:00Z"
77 | },
78 | "filters" : [
79 | {"field": "ndvContainers", "expression": "gt", "value": 0}
80 | ]
81 | }
82 | response = api_object.imageSummary.search(json)
83 | assert "data" in next(response, None)
--------------------------------------------------------------------------------
/tests/api/v2/test_vulnerability_exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.vulnerability_exceptions import VulnerabilityExceptionsAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.vulnerability_exceptions
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "exception_name": f"Test Container Exception {random_text}",
23 | "exception_reason": "Accepted Risk",
24 | "exception_type": "Container",
25 | "vulnerability_criteria": {
26 | "severity": ["Info", "Low"],
27 | "fixable": [0]
28 | },
29 | "props": {
30 | "description": f"Test Container Exception Description {random_text}"
31 | }
32 | }
33 |
34 |
35 | @pytest.fixture(scope="module")
36 | def api_object_update_body(random_text):
37 | return {
38 | "exception_name": f"Test Container Exception {random_text} (Updated)",
39 | "vulnerability_criteria": {
40 | "severity": ["Medium"]
41 | },
42 | "props": {
43 | "description": f"Test Container Exception Description {random_text} (Updated)"
44 | }
45 | }
46 |
47 |
48 | class TestVulnerabilityExceptions(CrudEndpoint):
49 |
50 | OBJECT_ID_NAME = "exceptionGuid"
51 | OBJECT_TYPE = VulnerabilityExceptionsAPI
52 |
53 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
54 | @pytest.mark.order("first")
55 | def test_api_get_by_guid(self, api_object):
56 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
57 |
--------------------------------------------------------------------------------
/tests/api/v2/test_vulnerability_policies.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import pytest
7 |
8 | from laceworksdk.api.v2.vulnerability_policies import VulnerabilityPoliciesAPI
9 | from tests.api.test_crud_endpoint import CrudEndpoint
10 |
11 |
12 | # Tests
13 |
14 | @pytest.fixture(scope="module")
15 | def api_object(api):
16 | return api.vulnerability_policies
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def api_object_create_body(random_text):
21 | return {
22 | "policy_type": "DockerFile",
23 | "policy_name": f"Test Container Policy {random_text}",
24 | "severity": "High",
25 | "state": True,
26 | "filter": {
27 | "rule": {
28 | "operator": "include",
29 | "values": [
30 | "test"
31 | ]
32 | }
33 | },
34 | "props": {
35 | "description": f"Test Container Policy Description {random_text}"
36 | }
37 | }
38 |
39 |
40 | @pytest.fixture(scope="module")
41 | def api_object_update_body(random_text):
42 | return {
43 | "policy_name": f"Test Container Exception {random_text} (Updated)",
44 | "severity": "Medium",
45 | "props": {
46 | "description": f"Test Container Exception Description {random_text} (Updated)"
47 | }
48 | }
49 |
50 |
51 | class TestVulnerabilityExceptions(CrudEndpoint):
52 |
53 | OBJECT_ID_NAME = "policyGuid"
54 | OBJECT_TYPE = VulnerabilityPoliciesAPI
55 |
56 | @pytest.mark.flaky(reruns=10) # Because sometimes this attempts to get an object that has just been deleted
57 | @pytest.mark.order("first")
58 | def test_api_get_by_guid(self, api_object):
59 | self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME)
60 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | pytest_plugins = [
7 | "tests.test_laceworksdk",
8 | "tests.api",
9 | ]
10 |
--------------------------------------------------------------------------------
/tests/environment.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test environment variables.
4 | """
5 |
6 | import os
7 |
8 | from dotenv import load_dotenv
9 |
10 | load_dotenv()
11 |
12 |
13 | LW_ACCOUNT = os.getenv("LW_ACCOUNT")
14 | if LW_ACCOUNT is None:
15 | raise RuntimeError(
16 | "You must set the 'LW_ACCOUNT' environment variable to run the test suite"
17 | )
18 |
19 | LW_API_KEY = os.getenv("LW_API_KEY")
20 | if LW_API_KEY is None:
21 | raise RuntimeError(
22 | "You must set the 'LW_API_KEY' environment variable to run the test suite"
23 | )
24 |
25 | LW_API_SECRET = os.getenv("LW_API_SECRET")
26 | if LW_API_SECRET is None:
27 | raise RuntimeError(
28 | "You must set the 'LW_API_SECRET' environment variable to run the test suite"
29 | )
30 |
--------------------------------------------------------------------------------
/tests/test_laceworksdk.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Test suite for the community-developed Python SDK for interacting with Lacework APIs.
4 | """
5 |
6 | import laceworksdk
7 |
8 |
9 | class TestLaceworkSDK:
10 | """Test the package-level code."""
11 |
12 | def test_package_contents(self):
13 | """Ensure the package contains the correct top-level objects."""
14 |
15 | # Lacework API Wrapper
16 | assert hasattr(laceworksdk, "LaceworkClient")
17 |
18 | # Lacework Exceptions
19 | assert hasattr(laceworksdk, "ApiError")
20 | assert hasattr(laceworksdk, "LaceworkSDKException")
21 |
--------------------------------------------------------------------------------