├── .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 | --------------------------------------------------------------------------------