diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 472064a1..a8dc027d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,20 +14,20 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - with: - path: code42cli - name: Setup Python uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} - name: Install tox - run: pip install tox==3.17.1 + run: | + pip install tox==3.17.1 + pip install . - name: Run Unit tests - run: cd code42cli; tox -e py # Run tox using the version of Python in `PATH` + run: tox -e py # Run tox using the version of Python in `PATH` - name: Submit coverage report uses: codecov/codecov-action@v1.0.7 with: @@ -43,7 +43,6 @@ jobs: 127.0.0.1 core 127.0.0.1 alerts 127.0.0.1 alert-rules - 127.0.0.1 detection-lists 127.0.0.1 audit-log 127.0.0.1 file-events 127.0.0.1 storage @@ -55,6 +54,6 @@ jobs: - name: Install ncat run: sudo apt-get install ncat - name: Start up the mock servers - run: cd code42-mock-servers; docker-compose up -d --build + run: cd code42-mock-servers; docker compose up -d --build - name: Run the integration tests - run: sleep 15; cd code42cli; tox -e integration + run: sleep 15; tox -e integration diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4afff063..89444967 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,8 +18,10 @@ jobs: - name: Setup Python uses: actions/setup-python@v1 with: - python-version: '3.x' + python-version: '3.11' - name: Install tox - run: pip install tox==3.17.1 + run: | + pip install tox==3.17.1 + pip install . - name: Build docs run: tox -e docs diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ea8a73ff..38ace65f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 @@ -28,7 +28,9 @@ jobs: ssh-agent -a $SSH_AUTH_SOCK > /dev/null ssh-add - <<< "${{ secrets.C42_EVENT_EXTRACTOR_PRIVATE_DEPLOY_KEY }}" - name: Install tox - run: pip install tox==3.17.1 + run: | + pip install tox==3.17.1 + pip install . - name: Run Unit tests env: SSH_AUTH_SOCK: /tmp/ssh_agent.sock diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 64645e65..55779389 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index dfc4dfcb..383e3196 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -18,8 +18,10 @@ jobs: - name: Setup Python uses: actions/setup-python@v1 with: - python-version: '3.x' + python-version: '3.11' - name: Install tox - run: pip install tox==3.17.1 + run: | + pip install tox==3.17.1 + pip install . - name: Run style checks run: tox -e style diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14e57479..c2871db6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 3.8.3 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cc1c33d8..186d4e54 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,17 @@ # Required version: 2 +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py @@ -13,7 +24,6 @@ sphinx: formats: all python: - version: 3.7 install: - method: pip path: . diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac452e4..f24b5e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The intended audience of this file is for py42 consumers -- as such, changes that don't affect how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here. +## 1.19.0 - 2025-03-21 + +### Deprecated + +- All Incydr functionality is deprecated in Code42CLI. Use the Incydr SDK instead: https://developer.code42.com/ + +## 1.18.1 - 2025-01-08 + +## Changed + +- Updated the user-agent prefix for compatibility with Incydr conventions. + +## Removed + +- Removed support for end-of-life python versions 3.6, 3.7, 3.8. + +## 1.18.0 - 2023-11-30 + +### Added + +- Support for Python 3.12, includes various dependency version requirement updates. + +## 1.17.0 - 2023-08-04 + +### Removed + +- Removed the following command groups following deprecation: + - `detection-lists` + - `departing-employee` + - `high-risk-employee` +- APIs were replaced by the `watchlists` commands + ## 1.16.6 - 2023-04-12 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5527425d..6ff27506 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,13 +50,13 @@ pyenv virtualenv 3.9.10 code42cli pyenv activate code42cli ``` -**Note**: The CLI supports pythons versions 3.6 through 3.9 for end users. However due to some of the build dependencies, you'll need a version >=3.7 for your virtual environment. Use `pyenv --versions` to see all versions available for install. There are some known issues installing python 3.6 with pyenv on certain OS. +**Note**: The CLI supports pythons versions 3.9 through 3.12 for end users. Use `pyenv --versions` to see all versions available for install. Use `source deactivate` to exit the virtual environment and `pyenv activate code42cli` to reactivate it. ### Windows/Linux -Install a version of python 3.6 or higher from [python.org](https://python.org). +Install a version of python 3.9 or higher from [python.org](https://python.org). Next, in a directory somewhere outside the project, create and activate your virtual environment: ```bash @@ -86,7 +86,7 @@ point to your virtual environment, and you should be ready to go! ## Run a full build -We use [tox](https://tox.readthedocs.io/en/latest/#) to run our build against Python 3.6, 3.7, and 3.8. When run locally, `tox` will run only against the version of python that your virtual envrionment is running, but all versions will be validated against when you [open a PR](#opening-a-pr). +We use [tox](https://tox.readthedocs.io/en/latest/#) to run our build against Python 3.9, 3.10, 3.11 and 3.12. When run locally, `tox` will run only against the version of python that your virtual envrionment is running, but all versions will be validated against when you [open a PR](#opening-a-pr). To run all the unit tests, do a test build of the documentation, and check that the code meets all style requirements, simply run: @@ -97,7 +97,7 @@ If the full process runs without any errors, your environment is set up correctl ## Coding Style -Use syntax and built-in modules that are compatible with Python 3.6+. +Use syntax and built-in modules that are compatible with Python 3.9+. ### Style linter diff --git a/README.md b/README.md index a3bb56d6..c0c485cb 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,19 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/code42cli/badge/?version=latest)](https://clidocs.code42.com/en/latest/?badge=latest) +## Code42CLI end-of-life +Code42CLI is now deprecated. It has been replaced by the [Incydr CLI](https://support.code42.com/hc/en-us/articles/14827667072279-Introduction-to-the-Incydr-command-line-interface). +- Code42CLI will reach **end-of-support on January 1, 2026**, and **end-of-life on January 1, 2027**. +- To ensure uninterrupted functionality and access to the latest features, migrate your integrations to the Incydr CLI as soon as possible. + +For more details, [see our FAQ](https://support.code42.com/hc/en-us/articles/32154640298263-Code42-CLI-end-of-life-FAQ). + Use the `code42` command to interact with your Code42 environment. * `code42 security-data` is a CLI tool for extracting AED events. Additionally, you can choose to only get events that Code42 previously did not observe since you last recorded a checkpoint (provided you do not change your query). -* `code42 high-risk-employee` is a collection of tools for managing the high risk employee detection list. Similarly, - there is `code42 departing-employee`. +* `code42 watchlists` is a collection of tools for managing your employee watchlists. ## Requirements @@ -212,38 +218,6 @@ To get the results of a saved search, use the `--saved-search` option with your code42 security-data search --saved-search ``` -## Detection Lists - -You can both add and remove employees from detection lists using the CLI. This example uses `high-risk-employee`. - -```bash -code42 high-risk-employee add user@example.com --notes "These are notes" -code42 high-risk-employee remove user@example.com -``` - -Detection lists include a `bulk` command. To add employees to a list, you can pass in a csv file. First, generate the -csv file for the desired command by executing the `generate-template` command: - -```bash -code42 high-risk-employee bulk generate-template add -``` - -Notice that `generate-template` takes a `cmd` parameter for determining what type of template to generate. In the -example above, we give it the value `add` to generate a file for bulk adding users to the high risk employee list. - -Next, fill out the csv file with all the users and then pass it in as a parameter to `bulk add`: - -```bash -code42 high-risk-employee bulk add users_to_add.csv -``` - -Note that for `bulk remove`, the file only has to be an end-line delimited list of users with one line per user. - -## Known Issues - -In `security-data`, only the first 10,000 of each set of events containing the exact same insertion timestamp is -reported. - ## Troubleshooting If you keep getting prompted for your password, try resetting with `code42 profile reset-pw`. diff --git a/docs/commands.md b/docs/commands.md index 4ebf3509..79c54b17 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -17,8 +17,6 @@ Trusted Activities Users Watchlists - (DEPRECATED) Departing Employee - (DEPRECATED) High Risk Employee ``` * [Alert Rules](commands/alertrules.rst) @@ -32,5 +30,3 @@ * [Trusted Activities](commands/trustedactivities.rst) * [Users](commands/users.rst) * [Watchlists](commands/watchlists.rst) -* [(DEPRECATED) Departing Employee](commands/departingemployee.rst) -* [(DEPRECATED) High Risk Employee](commands/highriskemployee.rst) diff --git a/docs/commands/alertrules.rst b/docs/commands/alertrules.rst index d8f2507c..cb0d9050 100644 --- a/docs/commands/alertrules.rst +++ b/docs/commands/alertrules.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.alert_rules:alert_rules :prog: alert-rules :nested: full diff --git a/docs/commands/alerts.rst b/docs/commands/alerts.rst index 4c39ea8b..96c7eb82 100644 --- a/docs/commands/alerts.rst +++ b/docs/commands/alerts.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.alerts:alerts :prog: alerts :nested: full diff --git a/docs/commands/auditlogs.rst b/docs/commands/auditlogs.rst index 29eb0e46..d2d70f43 100644 --- a/docs/commands/auditlogs.rst +++ b/docs/commands/auditlogs.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.auditlogs:audit_logs :prog: audit-logs :nested: full diff --git a/docs/commands/cases.rst b/docs/commands/cases.rst index ac124f0a..b2e5665a 100644 --- a/docs/commands/cases.rst +++ b/docs/commands/cases.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.cases:cases :prog: cases :nested: full diff --git a/docs/commands/departingemployee.rst b/docs/commands/departingemployee.rst deleted file mode 100644 index a1c3ac5e..00000000 --- a/docs/commands/departingemployee.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. click:: code42cli.cmds.departing_employee:departing_employee - :prog: departing-employee - :nested: full diff --git a/docs/commands/highriskemployee.rst b/docs/commands/highriskemployee.rst deleted file mode 100644 index 4fd75700..00000000 --- a/docs/commands/highriskemployee.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. click:: code42cli.cmds.high_risk_employee:high_risk_employee - :prog: high-risk-employee - :nested: full diff --git a/docs/commands/securitydata.rst b/docs/commands/securitydata.rst index f0eaa317..15c37a73 100644 --- a/docs/commands/securitydata.rst +++ b/docs/commands/securitydata.rst @@ -2,9 +2,7 @@ Security Data ************* -.. warning:: V1 file events, saved searches, and queries are **deprecated**. - -See more information in the `Enable V2 File Events User Guide <../userguides/v2apis.html>`_. +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. .. click:: code42cli.cmds.securitydata:security_data :prog: security-data diff --git a/docs/commands/trustedactivities.rst b/docs/commands/trustedactivities.rst index 67a11408..ff218d34 100644 --- a/docs/commands/trustedactivities.rst +++ b/docs/commands/trustedactivities.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.trustedactivities:trusted_activities :prog: trusted-activities :nested: full diff --git a/docs/commands/watchlists.rst b/docs/commands/watchlists.rst index 1b48ba24..b52b462b 100644 --- a/docs/commands/watchlists.rst +++ b/docs/commands/watchlists.rst @@ -1,3 +1,5 @@ +.. warning:: Incydr functionality is **deprecated**. Use the Incydr CLI instead. + .. click:: code42cli.cmds.watchlists:watchlists :prog: watchlists :nested: full diff --git a/docs/conf.py b/docs/conf.py index 94ced009..87a5ab36 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ ] # Add myst_parser types to suppress warnings -suppress_warnings = ["myst.header"] +suppress_warnings = ["myst.header", "myst.xref_missing"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -61,7 +61,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +# language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/guides.md b/docs/guides.md index 86e9bc4f..bbf07f09 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -8,31 +8,17 @@ Get started with the Code42 command-line interface (CLI) Configure a profile - Enable V2 File Events - Ingest data into a SIEM Manage legal hold users Clean up your environment by deactivating devices Write custom extension scripts using the Code42 CLI and Py42 Manage users - Configure trusted activities - Configure alert rules - Add and manage cases Perform bulk actions - Manage watchlist members - (DEPRECATED) Manage detection list users ``` * [Get started with the Code42 command-line interface (CLI)](userguides/gettingstarted.md) * [Configure a profile](userguides/profile.md) -* [Enable V2 File Events](userguides/v2apis.md) -* [Ingest data into a SIEM](userguides/siemexample.md) * [Manage legal hold users](userguides/legalhold.md) * [Clean up your environment by deactivating devices](userguides/deactivatedevices.md) * [Write custom extension scripts using the Code42 CLI and Py42](userguides/extensions.md) * [Manage users](userguides/users.md) -* [Configure trusted activities](userguides/trustedactivities.md) -* [Configure alert rules](userguides/alertrules.md) -* [Add and manage cases](userguides/cases.md) * [Perform bulk actions](userguides/bulkcommands.md) -* [Manage watchlist members](userguides/watchlists.md) -* [(DEPRECATED) Manage detection list users](userguides/detectionlists.md) diff --git a/docs/index.md b/docs/index.md index 51465879..c2899507 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,10 @@ commands ``` +```{eval-rst} +.. warning:: Incydr functionality in the code42cli is **deprecated**. Use the resources at https://developer.code42.com/ instead. +``` + [![license](https://img.shields.io/pypi/l/code42cli.svg)](https://pypi.org/project/code42cli/) [![versions](https://img.shields.io/pypi/pyversions/code42cli.svg)](https://pypi.org/project/code42cli/) diff --git a/docs/userguides/alertrules.md b/docs/userguides/alertrules.md deleted file mode 100644 index bc646262..00000000 --- a/docs/userguides/alertrules.md +++ /dev/null @@ -1,110 +0,0 @@ -# Add Users to Alert Rules - -Once you [create an alert rule in the Code42 console](https://support.code42.com/Administrator/Cloud/Code42_console_reference/Alert_rule_settings_reference), you can use the CLI `alert-rules` commands to add and remove users from your existing alert rules. - -To see a list of all the users currently in your organization: -- Export a list from the [Users action menu](https://support.code42.com/Administrator/Cloud/Code42_console_reference/Users_reference#Action_menu). -- Use the [CLI users commands](./users.md). - -## View Existing Alert Rules - -You'll need the ID of an alert rule to add or remove a user. - -To view a list of all alert rules currently created for your organization, including the rule ID, use the following command: -```bash -code42 alert-rules list -``` - -Once you've identified the rule ID, view the details of the alert rule as follows: -```bash -code42 alert-rules show -``` - -#### Example output -Example output for a single alert rule in default JSON format. -```json -{ - "type$": "ENDPOINT_EXFILTRATION_RULE_DETAILS_RESPONSE", - "rules": [ - { - "type$": "ENDPOINT_EXFILTRATION_RULE_DETAILS", - "tenantId": "c4e43418-07d9-4a9f-a138-29f39a124d33", - "name": "My Rule", - "description": "this is your rule!", - "severity": "HIGH", - "isEnabled": false, - "fileBelongsTo": { - "type$": "FILE_BELONGS_TO", - "usersToAlertOn": "ALL_USERS" - }, - "notificationConfig": { - "type$": "NOTIFICATION_CONFIG", - "enabled": false - }, - "fileCategoryWatch": { - "type$": "FILE_CATEGORY_WATCH", - "watchAllFiles": true - }, - "ruleSource": "Alerting", - "fileSizeAndCount": { - "type$": "FILE_SIZE_AND_COUNT", - "fileCountGreaterThan": 2, - "totalSizeGreaterThanInBytes": 200, - "operator": "AND" - }, - "fileActivityIs": { - "type$": "FILE_ACTIVITY", - "syncedToCloudService": { - "type$": "SYNCED_TO_CLOUD_SERVICE", - "watchBox": false, - "watchBoxDrive": false, - "watchDropBox": false, - "watchGoogleBackupAndSync": false, - "watchAppleIcLoud": false, - "watchMicrosoftOneDrive": false - }, - "uploadedOnRemovableMedia": true, - "readByBrowserOrOther": true - }, - "timeWindow": 15, - "id": "404ff012-fa2f-4acf-ae6d-107eabf7f24c", - "createdAt": "2021-04-27T01:55:36.4204590Z", - "createdBy": "sean.cassidy@example.com", - "modifiedAt": "2021-09-03T01:46:13.2902310Z", - "modifiedBy": "sean.cassidy@example.com", - "isSystem": false - } - ] -} -``` - -## Add a User to an Alert Rule - -You can manage the users who are associated with an alert rule once you know the rule's `rule_id` and the user's `username`. - -To add a single user to your alert rule, use the following command: -```bash -code42 alert-rules add-user --rule-id -u sean.cassidy@example.com -``` - -Alternatively, to add multiple users to your alert rule, fill out the `add` CSV file template, then use the `bulk add` command with the CSV file path. -```bash -code42 alert-rules bulk add users.csv -``` - -You can remove single or multiple users from alert rules similarly using the `remove-user` and `bulk remove` commands. - - -## Get CSV Template - -The following command will generate a CSV template to either add or remove users from multiple alert rules at once. The CSV file will be saved to the current working directory. -```bash -code42 alert-rules bulk generate-template [add|remove] -``` - -You can then fill out and use each of the CSV templates with their respective bulk commands. -```bash -code42 alert-rules bulk [add|remove] /Users/my_user/bulk-command.csv -``` - -Learn more about the [Alert Rules](../commands/alertrules.md) commands. diff --git a/docs/userguides/cases.md b/docs/userguides/cases.md deleted file mode 100644 index 06f72e05..00000000 --- a/docs/userguides/cases.md +++ /dev/null @@ -1,96 +0,0 @@ -# Add and Manage Cases - -To create a new case, only the name is required. Other attributes are optional and can be provided through the available flags. - -The following command creates a case with the `subject` and `assignee` user indicated by their respective UIDs. -```bash -code42 cases create My-Case --subject 123 --assignee 456 --description "Sample case" -``` - -## Update a Case - -To further update or view the details of your case, you'll need the case's unique number, which is assigned upon creation. To get this number, you can use the `list` command to view all cases, with optional filter values. - -To print to the console all open cases created in the last 30 days: -```bash -code42 cases list --begin-create-time 30d --status OPEN -``` - -#### Example Output -Example output for a single case in JSON format. -```json -{ - "number": 42, - "name": "My-Case", - "createdAt": "2021-9-17T18:29:53.375136Z", - "updatedAt": "2021-9-17T18:29:53.375136Z", - "description": "Sample case", - "findings": "", - "subject": "123", - "subjectUsername": "sean.cassidy@example.com", - "status": "OPEN", - "assignee": "456", - "assigneeUsername": "elvis.presley@example.com", - "createdByUserUid": "789", - "createdByUsername": "andy.warhol@example.com", - "lastModifiedByUserUid": "789", - "lastModifiedByUsername": "andy.warhol@example.com" -} -``` - -Once you've identified your case's number, you can view further details on the case, or update its attributes. - -The following command will print all details of your case. -```bash -code42 cases show 42 -``` - -If you've finished your investigation and you'd like to close your case, you can update the status of the case. Similarly, other attributes of the case can be updated using the optional flags. -```bash -code42 cases update 42 --status CLOSED -``` - -## Get CSV Template - -The following command will generate a CSV template to either add or remove file events from multiple cases at once. The csv file will be saved to the current working directory. -```bash -code42 cases file-events bulk generate-template [add|remove] -``` - -You can then fill out and use each of the CSV templates with their respective bulk commands. -```bash -code42 cases file-events bulk [add|remove] bulk-command.csv -``` - -## Manage File Exposure Events Associated with a Case - -The following example command can be used to view all the file exposure events currently associated with a case, indicated here by case number `42`. -```bash -code42 cases file-events list 42 -``` - -Use the `file-events add` command to associate a single file event, referred to by event ID, to a case. - -Below is an example command to associate some event with ID `event_abc` with case number `42`. -```bash -code42 cases file-events add 42 event_abc -``` - -To associate multiple file events with one or more cases at once, enter the case and file event information into the `file-events add` CSV file template, then use the `bulk add` command with the CSV file path. For example: -```bash -code42 cases file-events bulk add my_new_cases.csv -``` - -Similarly, the `file-events remove` and `file-events bulk remove` commands can be used to remove a file event from a case. - -## Export Case Details - -You can use the CLI to export the details of a case into a PDF. - -The following example command will download the details from case number `42` and save a PDF with the name `42_case_summary.pdf` to the provided path. If a path is not provided, it will be saved to the current working directory. - -```bash -code42 cases export 42 --path /Users/my_user/cases/ -``` - -Learn more about the [Managing Cases](../commands/cases.md). diff --git a/docs/userguides/detectionlists.md b/docs/userguides/detectionlists.md deleted file mode 100644 index ce96c547..00000000 --- a/docs/userguides/detectionlists.md +++ /dev/null @@ -1,62 +0,0 @@ -# (DEPRECATED) Manage Detection List Users - -```{eval-rst} -.. note:: - - Detection Lists have been replaced by Watchlists. - - Functionality for adding users to Departing Employee and High Risk Employee categories has been migrated to the :code:`code42 watchlists` command group. - - Functionality for listing and managing User Risk Profiles (e.g. adding Cloud Aliases, Notes, and Start/End dates to a user profile) has been migrated to the :code:`code42 users` command group. -``` - -Use the `departing-employee` commands to add employees to or remove employees from the Departing Employees list. Use the `high-risk-employee` commands to add employees to or remove employees from the High Risk list, or update risk tags for those users. - -To see a list of all the users currently in your organization: -- Export a list from the [Users action menu](https://support.code42.com/Administrator/Cloud/Code42_console_reference/Users_reference#Action_menu). -- Use the [CLI users commands](./users.md). - -## Get CSV template -To add multiple users to the Departing Employees list: - -1. Generate a CSV template. Below is an example command for generating a template to use to add employees to the Departing -Employees list. Once generated, the CSV file is saved to your current working directory. - -```bash -code42 departing-employee bulk generate-template add -``` - -2. Use the CSV template to enter the employees' information. Only the Code42 username is required. If added, -the departure date must be in yyyy-MM-dd format. Note: you are only able to add departure dates during the `add` -operation. If you don't include `--departure-date`, you can only add one later by removing and then re-adding the -employee. - -3. Save the CSV file. - -## Add users to the Departing Employees list - -Once you have entered the employees' information in the CSV file, use the `bulk add` command with the CSV file path to -add multiple users at once. For example: - -```bash -code42 departing-employee bulk add /Users/astrid.ludwig/add_departing_employee.csv -``` - -## Remove users -You can remove one or more users from the High Risk Employees list. Use `code42 departing-employee remove` to remove a -single user. - -To remove multiple users at once: - -1. Create a CSV file with one username per line. - -2. Save the file to your current working directory. - -3. Use the `bulk remove` command. For example: - -```bash -code42 high-risk-employee bulk remove /Users/matt.allen/remove_high_risk_employee.csv -``` - -Learn more about the [Departing Employee](../commands/departingemployee.md) and -[High Risk Employee](../commands/highriskemployee.md) commands. diff --git a/docs/userguides/siemexample.md b/docs/userguides/siemexample.md deleted file mode 100644 index 4cfdbfa8..00000000 --- a/docs/userguides/siemexample.md +++ /dev/null @@ -1,273 +0,0 @@ -# Ingest file event data or alerts into a SIEM tool - -This guide provides instructions on using the CLI to ingest Code42 file event data or alerts -into a security information and event management (SIEM) tool like LogRhythm, Sumo Logic, or IBM QRadar. - -## Considerations - -To ingest file events or alerts into a SIEM tool using the Code42 command-line interface, the Code42 user account running the integration -must be assigned roles that provide the necessary permissions. - -The CEF format is not recommended because it was not designed for insider risk event data. Code42 file event data contains many fields that provide valuable insider risk context that have no CEF equivalent. However, if you need to use CEF, the JSON-to-CEF mapping at the bottom of this document indicates which fields are included and how the field names map to other formats. - -## Before you begin - -First install and configure the Code42 CLI following the instructions in -[Getting Started](gettingstarted.md). - -## Run queries -You can get file events in either a JSON or CEF format for use by your SIEM tool. Alerts data and audit logs are available in JSON format. You can query the data as a -scheduled job or run ad-hoc queries. - -Learn more about searching [File Events](../commands/securitydata.md), [Alerts](../commands/alerts.md), and [Audit Logs](../commands/auditlogs.md) using the CLI. - -### Run a query as a scheduled job - -Use your favorite scheduling tool, such as cron or Windows Task Scheduler, to run a query on a regular basis. Specify -the profile to use by including `--profile`. - -#### File Exposure Events -An example using the `send-to` command to forward only the new file event data since the previous request to an external syslog server: -```bash -code42 security-data send-to syslog.example.com:514 -p UDP --profile profile1 -c syslog_sender -``` -#### Alerts -An example to send to the syslog server only the new alerts that meet the filter criteria since the previous request: -```bash -code42 alerts send-to syslog.example.com:514 -p UDP --profile profile1 --rule-name "Source code exfiltration" --state OPEN -i -``` -#### Audit Logs -An example to send to the syslog server only the audit log events that meet the filter criteria from the last 30 days. -```bash -code42 audit-logs send-to syslog.example.com:514 -p UDP --profile profile1 --actor-username 'sean.cassidy@example.com' -b 30d -``` - -As a best practice, use a separate profile when executing a scheduled task. Using separate profiles can help prevent accidental updates to your stored checkpoints, for example, by adding `--use-checkpoint` to adhoc queries. - -### Run an ad-hoc query - -Examples of ad-hoc queries you can run are as follows. - -#### File Exposure Events - -Print file events since March 5 for a user in raw JSON format: -```bash -code42 security-data search -f RAW-JSON -b 2020-03-05 --c42-username 'sean.cassidy@example.com' -``` - -Print file events since March 5 where a file was synced to a cloud service: -```bash -code42 security-data search -t CloudStorage -b 2020-03-05 -``` - -Write to a text file the file events in raw JSON format where a file was read by browser or other app for a user since -March 5: -```bash -code42 security-data search -f RAW-JSON -b 2020-03-05 -t ApplicationRead --c42-username 'sean.cassidy@example.com' > /Users/sangita.maskey/Downloads/c42cli_output.txt -``` -#### Alerts -Print alerts since May 5 where a file's cloud share permissions changed: -```bash -code42 alerts print -b 2020-05-05 --rule-type FedCloudSharePermissions -``` -#### Audit Logs -Print audit log events since June 5 which affected a certain user: -```bash -code42 audit-logs search -b 2021-06-05 --affected-username 'sean.cassidy@examply.com' -``` - -#### Example Outputs - -Example output for a single file exposure event (in default JSON format): - -```json -{ - "eventId": "0_c4b5e830-824a-40a3-a6d9-345664cfbb33_942704829036142720_944009394534374185_342", - "eventType": "CREATED", - "eventTimestamp": "2020-03-05T14:45:49.662Z", - "insertionTimestamp": "2020-03-05T15:10:47.930Z", - "filePath": "C:/Users/sean.cassidy/Google Drive/", - "fileName": "1582938269_Longfellow_Cloud_Arch_Redesign.drawio", - "fileType": "FILE", - "fileCategory": "DOCUMENT", - "fileSize": 6025, - "fileOwner": "Administrators", - "md5Checksum": "9ab754c9133afbf2f70d5fe64cde1110", - "sha256Checksum": "8c6ba142065373ae5277ecf9f0f68ab8f9360f42a82eb1dec2e1816d93d6b1b7", - "createTimestamp": "2020-03-05T14:29:33.455Z", - "modifyTimestamp": "2020-02-29T01:04:31Z", - "deviceUserName": "sean.cassidy@example.com", - "osHostName": "LAPTOP-091", - "domainName": "192.168.65.129", - "publicIpAddress": "71.34.10.80", - "privateIpAddresses": [ - "fe80:0:0:0:8d61:ec3f:9e32:2efc%eth2", - "192.168.65.129", - "0:0:0:0:0:0:0:1", - "127.0.0.1" - ], - "deviceUid": "942704829036142720", - "userUid": "887050325252344565", - "source": "Endpoint", - "exposure": [ - "CloudStorage" - ], - "syncDestination": "GoogleBackupAndSync" -} -``` -Example output for a single alert (in default JSON format): - -```json -{ - "type$": "ALERT_DETAILS", - "tenantId": "c4b5e830-824a-40a3-a6d9-345664cfbb33", - "type": "FED_CLOUD_SHARE_PERMISSIONS", - "name": "Cloud Share", - "description": "Alert Rule for data exfiltration via Cloud Share", - "actor": "leland.stewart@example.com", - "target": "N/A", - "severity": "HIGH", - "ruleId": "408eb1ae-587e-421a-9444-f75d5399eacb", - "ruleSource": "Alerting", - "id": "7d936d0d-e783-4b24-817d-f19f625e0965", - "createdAt": "2020-05-22T09:47:33.8863230Z", - "state": "OPEN", - "observations": [{"type$": "OBSERVATION", - "id": "4bc378e6-bfbd-40f0-9572-6ed605ea9f6c", - "observedAt": "2020-05-22T09:40:00.0000000Z", - "type": "FedCloudSharePermissions", - "data": { - "type$": "OBSERVED_CLOUD_SHARE_ACTIVITY", - "id": "4bc378e6-bfbd-40f0-9572-6ed605ea9f6c", - "sources": ["GoogleDrive"], - "exposureTypes": ["PublicLinkShare"], - "firstActivityAt": "2020-05-22T09:40:00.0000000Z", - "lastActivityAt": "2020-05-22T09:45:00.0000000Z", - "fileCount": 1, - "totalFileSize": 6025, - "fileCategories": [{"type$": "OBSERVED_FILE_CATEGORY", "category": "Document", "fileCount": 1, "totalFileSize": 6025, "isSignificant": false}], - "files": [{"type$": "OBSERVED_FILE", "eventId": "1hHdK6Qe6hez4vNCtS-UimDf-sbaFd-D7_3_baac33d0-a1d3-4e0a-9957-25632819eda7", "name": "1590140395_Longfellow_Cloud_Arch_Redesign.drawio", "category": "Document", "size": 6025}], - "outsideTrustedDomainsEmailsCount": 0, "outsideTrustedDomainsTotalDomainCount": 0, "outsideTrustedDomainsTotalDomainCountTruncated": false}}] -} -``` - -Example output for a single audit log event (in default JSON format): -```json -{ - "type$": "audit_log::logged_in/1", - "actorId": "1015070955620029617", - "actorName": "sean.cassidy@example.com", - "actorAgent": "py42 1.17.0 python 3.7.10", - "actorIpAddress": "67.220.16.122", - "timestamp": "2021-08-30T16:16:19.165Z", - "actorType": "USER" -} -``` - - -## CEF Mapping - -The following tables map the file event data from the Code42 CLI to common event format (CEF). - -### Attribute mapping - -The table below maps JSON fields, CEF fields, and [Forensic Search fields](https://code42.com/r/support/forensic-search-fields) -to one another. - -```{eval-rst} - -+----------------------------+---------------------------------+----------------------------------------+ -| JSON field | CEF field | Forensic Search field | -+============================+=================================+========================================+ -| actor | suser | Actor | -+----------------------------+---------------------------------+----------------------------------------+ -| cloudDriveId | aid | n/a | -+----------------------------+---------------------------------+----------------------------------------+ -| createTimestamp | fileCreateTime | File Created Date | -+----------------------------+---------------------------------+----------------------------------------+ -| deviceUid | deviceExternalId | n/a | -+----------------------------+---------------------------------+----------------------------------------+ -| deviceUserName | suser | Username (Code42) | -+----------------------------+---------------------------------+----------------------------------------+ -| domainName | dvchost | Fully Qualified Domain Name | -+----------------------------+---------------------------------+----------------------------------------+ -| eventId | externalID | n/a | -+----------------------------+---------------------------------+----------------------------------------+ -| eventTimestamp | end | Date Observed | -+----------------------------+---------------------------------+----------------------------------------+ -| exposure | reason | Exposure Type | -+----------------------------+---------------------------------+----------------------------------------+ -| fileCategory | fileType | File Category | -+----------------------------+---------------------------------+----------------------------------------+ -| fileName | fname | Filename | -+----------------------------+---------------------------------+----------------------------------------+ -| filePath | filePath | File Path | -+----------------------------+---------------------------------+----------------------------------------+ -| fileSize | fsize | File Size | -+----------------------------+---------------------------------+----------------------------------------+ -| insertionTimestamp | rt | n/a | -+----------------------------+---------------------------------+----------------------------------------+ -| md5Checksum | fileHash | MD5 Hash | -+----------------------------+---------------------------------+----------------------------------------+ -| modifyTimestamp | fileModificationTime | File Modified Date | -+----------------------------+---------------------------------+----------------------------------------+ -| osHostName | shost | Hostname | -+----------------------------+---------------------------------+----------------------------------------+ -| processName | sproc | Executable Name (Browser or Other App) | -+----------------------------+---------------------------------+----------------------------------------+ -| processOwner | spriv | Process User (Browser or Other App) | -+----------------------------+---------------------------------+----------------------------------------+ -| publiclpAddress | src | IP Address (public) | -+----------------------------+---------------------------------+----------------------------------------+ -| removableMediaBusType | cs1, | Device Bus Type (Removable Media) | -| | Code42AEDRemovableMediaBusType | | -+----------------------------+---------------------------------+----------------------------------------+ -| removableMediaCapacity | cn1, | Device Capacity (Removable Media) | -| | Code42AEDRemovableMediaCapacity | | -+----------------------------+---------------------------------+----------------------------------------+ -| removableMediaName | cs3, | Device Media Name (Removable Media) | -| | Code42AEDRemovableMediaName | | -+----------------------------+---------------------------------+----------------------------------------+ -| removableMediaSerialNumber | cs4 | Device Serial Number (Removable Media) | -+----------------------------+---------------------------------+----------------------------------------+ -| removableMediaVendor | cs2, | Device Vendor (Removable Media) | -| | Code42AEDRemovableMediaVendor | | -+----------------------------+---------------------------------+----------------------------------------+ -| sharedWith | duser | Shared With | -+----------------------------+---------------------------------+----------------------------------------+ -| syncDestination | destinationServiceName | Sync Destination (Cloud) | -+----------------------------+---------------------------------+----------------------------------------+ -| url | filePath | URL | -+----------------------------+---------------------------------+----------------------------------------+ -| userUid | suid | n/a | -+----------------------------+---------------------------------+----------------------------------------+ -| windowTitle | requestClientApplication | Tab/Window Title | -+----------------------------+---------------------------------+----------------------------------------+ -| tabUrl | request | Tab URL | -+----------------------------+---------------------------------+----------------------------------------+ -| emailSender | suser | Sender | -+----------------------------+---------------------------------+----------------------------------------+ -| emailRecipients | duser | Recipients | -+----------------------------+---------------------------------+----------------------------------------+ -``` - -### Event mapping - -See the table below to map file events to CEF signature IDs. - -```{eval-rst} - -+--------------------+-----------+ -| Exfiltration event | CEF field | -+====================+===========+ -| CREATED | C42200 | -+--------------------+-----------+ -| MODIFIED | C42201 | -+--------------------+-----------+ -| DELETED | C42202 | -+--------------------+-----------+ -| READ_BY_APP | C42203 | -+--------------------+-----------+ -| EMAILED | C42204 | -+--------------------+-----------+ -``` diff --git a/docs/userguides/trustedactivities.md b/docs/userguides/trustedactivities.md deleted file mode 100644 index a40daa6f..00000000 --- a/docs/userguides/trustedactivities.md +++ /dev/null @@ -1,74 +0,0 @@ -# Configure Trusted Activities - -You can add trusted activities to your organization to prevent file activity associated with these locations from appearing in your security event dashboards, user profiles, and alerts. - -## Get CSV Template - -The following command generates a CSV template to either create, update, or remove multiple trusted activities at once. The CSV file is saved to the current working directory. -```bash -code42 trusted-activities bulk generate-template [create|update|remove] -``` - -You can then fill out and use each of the CSV templates with their respective bulk commands. -```bash -code42 trusted-activities bulk [create|update|remove] bulk-command.csv -``` - -## Add a New Trusted Activity - -Use the `create` command to add a new trusted domain or Slack workspace to your organization's trusted activities. -```bash -code42 trusted-activities create DOMAIN mydomain.com --description "a new trusted activity" -``` - -To add multiple trusted activities at once, enter information about the trusted activity into the `create` CSV file template. -For each activity, the `type` and `value` fields are required. - - `type` indicates the category of activity: - - `DOMAIN` indicates a trusted domain - - `SLACK` indicates a trusted Slack workspace - - `value` indicates either the name of the domain or Slack workspace. - -Then use the `bulk create` command with the CSV file path. For example: -```bash -code42 trusted-activities bulk create create_trusted_activities.csv -``` - -## Update a Trusted Activity - -Use the `update` command to update either the value or description of a single trusted activity. The `resource_id` of the activity is required. The other fields are optional. - -```bash -code42 trusted-activities update 123 --value my-updated-domain.com --description "an updated trusted activity" -``` - -To update multiple trusted activities at once, enter information about the trusted activity into the `update` CSV file template, then use the `bulk update` command with the CSV file path. - -```bash -code42 trusted-activities bulk update update_trusted_activities.csv -``` - -```{eval-rst} -.. note:: - The ``bulk update`` command cannot be used to clear the description of a trusted activity because you cannot indicate an empty string in a CSV format. - Pass an empty string to the ``description`` option of the ``update`` command to clear the description of a trusted activity. - - For example: ``code42 trusted-activities update 123 --description ""`` -``` - -## Remove a Trusted Activity - -Use the `remove` command to remove a single trusted activity. Only the `resource_id` of an activity is required to remove it. - -```bash -code42 trusted-activities remove 123 -``` - -To remove multiple trusted activities at once, enter information about the trusted activity into the `remove` CSV file template, then use the `bulk remove` command with the CSV file path. - -```bash -code42 trusted-activities bulk remove remove_trusted_activities.csv -``` - -Learn more about the [Trusted Activities](../commands/trustedactivities.md) commands. diff --git a/docs/userguides/v2apis.md b/docs/userguides/v2apis.md deleted file mode 100644 index 59366a15..00000000 --- a/docs/userguides/v2apis.md +++ /dev/null @@ -1,187 +0,0 @@ -# V2 File Events - -```{eval-rst} -.. warning:: V1 file events, saved searches, and queries are **deprecated**. -``` - -For details on the updated File Event Model, see the V2 File Events API documentation on the [Developer Portal](https://developer.code42.com/api/#tag/File-Events). - -V1 file event APIs were marked deprecated in May 2022 and will be no longer be supported after May 2023. - -Use the `--use-v2-file-events True` option with the `code42 profile create` or `code42 profile update` commands to enable your code42 CLI profile to use the latest V2 file event data model. - -Use `code42 profile show` to check the status of this setting on your profile: - -```bash -% code42 profile update --use-v2-file-events True - -% code42 profile show - -test-user-profile: - * username = test-user@code42.com - * authority url = https://console.core-int.cloud.code42.com - * ignore-ssl-errors = False - * use-v2-file-events = True - -``` - -For details on setting up a profile, see the [profile set up user guide](./profile.md). - -Enabling this setting will use the V2 data model for querying searches and saved searches with all `code security-data` commands. -The response shape for these events has changed from V1 and contains various field remappings, renamings, additions and removals. Column names will also be different when using the `Table` format for outputting events. - -### V2 File Event Data Example ### - -Below is an example of the new file event data model: - -```json -{ - "@timestamp": "2022-07-14T16:53:06.112Z", - "event": { - "id": "0_c4e43418-07d9-4a9f-a138-29f39a124d33_1068825680073059134_1068826271084047166_1_EPS", - "inserted": "2022-07-14T16:57:00.913917Z", - "action": "application-read", - "observer": "Endpoint", - "shareType": [], - "ingested": "2022-07-14T16:55:04.723Z", - "relatedEvents": [] - }, - "user": { - "email": "engineer@example.com", - "id": "1068824450489230065", - "deviceUid": "1068825680073059134" - }, - "file": { - "name": "cat.jpg", - "directory": "C:/Users/John Doe/Downloads/", - "category": "Spreadsheet", - "mimeTypeByBytes": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "categoryByBytes": "Spreadsheet", - "mimeTypeByExtension": "image/jpeg", - "categoryByExtension": "Image", - "sizeInBytes": 4748, - "owner": "John Doe", - "created": "2022-07-14T16:51:06.186Z", - "modified": "2022-07-14T16:51:07.419Z", - "hash": { - "md5": "8872dfa1c181b823d2c00675ae5926fd", - "sha256": "14d749cce008711b4ad1381d84374539560340622f0e8b9eb2fe3bba77ddbd64", - "md5Error": null, - "sha256Error": null - }, - "id": null, - "url": null, - "directoryId": [], - "cloudDriveId": null, - "classifications": [] - }, - "report": { - "id": null, - "name": null, - "description": null, - "headers": [], - "count": null, - "type": null - }, - "source": { - "category": "Device", - "name": "DESKTOP-1", - "domain": "192.168.00.000", - "ip": "50.237.00.00", - "privateIp": [ - "192.168.00.000", - "127.0.0.1" - ], - "operatingSystem": "Windows 10", - "email": { - "sender": null, - "from": null - }, - "removableMedia": { - "vendor": null, - "name": null, - "serialNumber": null, - "capacity": null, - "busType": null, - "mediaName": null, - "volumeName": [], - "partitionId": [] - }, - "tabs": [], - "domains": [] - }, - "destination": { - "category": "Cloud Storage", - "name": "Dropbox", - "user": { - "email": [] - }, - "ip": null, - "privateIp": [], - "operatingSystem": null, - "printJobName": null, - "printerName": null, - "printedFilesBackupPath": null, - "removableMedia": { - "vendor": null, - "name": null, - "serialNumber": null, - "capacity": null, - "busType": null, - "mediaName": null, - "volumeName": [], - "partitionId": [] - }, - "email": { - "recipients": null, - "subject": null - }, - "tabs": [ - { - "title": "Files - Dropbox and 1 more page - Profile 1 - Microsoft​ Edge", - "url": "https://www.dropbox.com/home", - "titleError": null, - "urlError": null - } - ], - "accountName": null, - "accountType": null, - "domains": [ - "dropbox.com" - ] - }, - "process": { - "executable": "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", - "owner": "John doe" - }, - "risk": { - "score": 17, - "severity": "CRITICAL", - "indicators": [ - { - "name": "First use of destination", - "weight": 3 - }, - { - "name": "File mismatch", - "weight": 9 - }, - { - "name": "Spreadsheet", - "weight": 0 - }, - { - "name": "Remote", - "weight": 0 - }, - { - "name": "Dropbox upload", - "weight": 5 - } - ], - "trusted": false, - "trustReason": null - } -} - -``` diff --git a/docs/userguides/watchlists.md b/docs/userguides/watchlists.md deleted file mode 100644 index b269a196..00000000 --- a/docs/userguides/watchlists.md +++ /dev/null @@ -1,76 +0,0 @@ -# Manage watchlist members - -## List created watchlists - -To list all the watchlists active in your Code42 environment, run: - -```bash -code42 watchlists list -``` - -## List all members of a watchlist - -You can list watchlists either by their Type: - -```bash -code42 watchlists list-members --watchlist-type DEPARTING_EMPLOYEE -``` - -or by their ID (get watchlist IDs from `code42 watchlist list` output): - -```bash -code42 watchlists list-members --watchlist-id 6e6c5acc-2568-4e5f-8324-e73f2811fa7c -``` - -A "member" of a watchlist is any user that the watchlist alerting rules apply to. Users can be members of a watchlist -either by being explicitly added (via console or `code42 watchlists add [USER_ID|USERNAME]`), but they can also be -implicitly included based on some user profile property (like working in a specific department). To get a list of only -those "members" who have been explicitly added (and thus can be removed via the `code42 watchlists remove [USER_ID|USERNAME]` -command), add the `--only-included-users` option to `list-members`. - -## Add or remove a single user from watchlist membership - -A user can be added to a watchlist using either the watchlist ID or Type, just like listing watchlists, and the user -can be identified either by their user_id or their username: - -```bash -code42 watchlist add --watchlist-type NEW_EMPLOYEE 9871230 -``` - -```bash -code42 watchlist add --watchlist-id 6e6c5acc-2568-4e5f-8324-e73f2811fa7c user@example.com -``` - -## Bulk adding/removing users from watchlists - -The bulk watchlist commands read input from a CSV file. - -Like the individual commands, they can take either a user_id/username or watchlist_id/watchlist_type to identify who -to add to which watchlist. Because of this flexibility, the CSV does require a header row identifying each column. - -You can generate a template CSV with the correct header values using the command: - -```bash -code42 watchlists bulk generate-template [add|remove] -``` - -If both username and user_id are provided in the CSV row, the user_id value will take precedence. If watchlist_type and watchlist_id columns -are both provided, the watchlist_id will take precedence. - -```{eval-rst} -.. note:: - - For watchlists that track additional metadata for a user (e.g. the "departure date" for a user on the Departing watchlist), that data - can be added/updated via the `code42 users bulk update-risk-profile <../commands/users.html#users-bulk-update-risk-profile>`_ command. - - You can re-use the same CSV file for both commands, just add the required risk profile columns to the CSV. - - For example, to bulk add users to multiple watchlists, with appropriate ``start_date``, ``end_date``, and ``notes`` values, create a CSV (in this example named ``watchlists.csv``) with the following:: - - username,watchlist_type,start_date,end_date,notes - user_a@example.com,DEPARTING_EMPLOYEE,,2023-10-10, - user_b@example.com,NEW_EMPLOYEE,2022-07-04,,2022 Summer Interns - - Then run ``code42 watchlists bulk add watchlists.csv`` - followed by ``code42 users bulk update-risk-profile watchlists.csv`` -``` diff --git a/setup.cfg b/setup.cfg index 22b1db08..f104e210 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,5 +31,7 @@ ignore = B904 # manual quoting B907 + # assertRaises-type + B908 # up to 88 allowed by bugbear B950 max-line-length = 80 diff --git a/setup.py b/setup.py index 04663e40..1fd44326 100644 --- a/setup.py +++ b/setup.py @@ -29,10 +29,10 @@ package_dir={"": "src"}, include_package_data=True, zip_safe=False, - python_requires=">=3.6.2, <4", + python_requires=">=3.9, <4", install_requires=[ "chardet", - "click>=7.1.1", + "click>=7.1.1,<8.2", "click_plugins>=1.1.1", "colorama>=0.4.3", "keyring==18.0.1", @@ -40,20 +40,22 @@ "ipython>=7.16.3;python_version<'3.8'", "ipython>=8.10.0;python_version>='3.8'", "pandas>=1.1.3", - "py42>=1.26.0", + "py42>=1.28.0", + "setuptools>=66.0.0", ], extras_require={ "dev": [ - "flake8==3.8.3", + "flake8>=4.0.0", "pytest==4.6.11", "pytest-cov==2.10.0", "pytest-mock==2.0.0", "tox>=3.17.1", + "importlib-metadata<5.0", ], "docs": [ - "sphinx==4.4.0", - "myst-parser==0.16", - "sphinx_rtd_theme==1.0.0", + "sphinx==8.1.3", + "myst-parser==4.0.0", + "sphinx_rtd_theme==3.0.2", "sphinx-click", ], }, @@ -63,9 +65,10 @@ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", ], entry_points={"console_scripts": ["code42=code42cli.main:cli"]}, diff --git a/src/code42cli/__version__.py b/src/code42cli/__version__.py index 032e9cb4..d84d79d4 100644 --- a/src/code42cli/__version__.py +++ b/src/code42cli/__version__.py @@ -1 +1 @@ -__version__ = "1.16.6" +__version__ = "1.19.0" diff --git a/src/code42cli/cmds/alert_rules.py b/src/code42cli/cmds/alert_rules.py index 0e034eed..294bdf61 100644 --- a/src/code42cli/cmds/alert_rules.py +++ b/src/code42cli/cmds/alert_rules.py @@ -15,6 +15,9 @@ from code42cli.options import format_option from code42cli.options import sdk_options from code42cli.output_formats import OutputFormatter +from code42cli.util import deprecation_warning + +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead." class AlertRuleTypes: @@ -35,7 +38,8 @@ class AlertRuleTypes: @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def alert_rules(state): - """Manage users associated with alert rules.""" + """DEPRECATED - Manage users associated with alert rules.""" + deprecation_warning(DEPRECATION_TEXT) pass diff --git a/src/code42cli/cmds/alerts.py b/src/code42cli/cmds/alerts.py index 6d90ac03..314e9d76 100644 --- a/src/code42cli/cmds/alerts.py +++ b/src/code42cli/cmds/alerts.py @@ -26,10 +26,13 @@ from code42cli.file_readers import read_csv_arg from code42cli.options import format_option from code42cli.output_formats import OutputFormatter +from code42cli.util import deprecation_warning from code42cli.util import hash_event from code42cli.util import parse_timestamp from code42cli.util import warn_interrupt +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." + ALERTS_KEYWORD = "alerts" ALERT_PAGE_SIZE = 25 @@ -194,7 +197,8 @@ def filter_options(f): @click.group(cls=OrderedGroup) @opt.sdk_options(hidden=True) def alerts(state): - """Get and send alert data.""" + """DEPRECATED - Get and send alert data.""" + deprecation_warning(DEPRECATION_TEXT) # store cursor getter on the group state so shared --begin option can use it in validation state.cursor_getter = _get_alert_cursor_store diff --git a/src/code42cli/cmds/auditlogs.py b/src/code42cli/cmds/auditlogs.py index 68f843cd..0671cde7 100644 --- a/src/code42cli/cmds/auditlogs.py +++ b/src/code42cli/cmds/auditlogs.py @@ -10,10 +10,13 @@ from code42cli.options import format_option from code42cli.options import sdk_options from code42cli.output_formats import OutputFormatter +from code42cli.util import deprecation_warning from code42cli.util import hash_event from code42cli.util import parse_timestamp from code42cli.util import warn_interrupt +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." + EVENT_KEY = "events" AUDIT_LOGS_KEYWORD = "audit-logs" @@ -90,7 +93,8 @@ def filter_options(f): @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def audit_logs(state): - """Get and send audit log event data.""" + """DEPRECATED - Get and send audit log event data.""" + deprecation_warning(DEPRECATION_TEXT) # store cursor getter on the group state so shared --begin option can use it in validation state.cursor_getter = _get_audit_log_cursor_store diff --git a/src/code42cli/cmds/cases.py b/src/code42cli/cmds/cases.py index 99e518af..199cb7d1 100644 --- a/src/code42cli/cmds/cases.py +++ b/src/code42cli/cmds/cases.py @@ -18,6 +18,9 @@ from code42cli.options import set_begin_default_dict from code42cli.options import set_end_default_dict from code42cli.output_formats import OutputFormatter +from code42cli.util import deprecation_warning + +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." case_number_arg = click.argument("case-number", type=int) @@ -74,7 +77,8 @@ def _get_events_header(): @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def cases(state): - """Manage cases and events associated with cases.""" + """DEPRECATED - Manage cases and events associated with cases.""" + deprecation_warning(DEPRECATION_TEXT) pass diff --git a/src/code42cli/cmds/departing_employee.py b/src/code42cli/cmds/departing_employee.py deleted file mode 100644 index 5a683f45..00000000 --- a/src/code42cli/cmds/departing_employee.py +++ /dev/null @@ -1,183 +0,0 @@ -import click -from py42.services.detectionlists.departing_employee import DepartingEmployeeFilters - -from code42cli.bulk import generate_template_cmd_factory -from code42cli.bulk import run_bulk_process -from code42cli.click_ext.groups import OrderedGroup -from code42cli.cmds.detectionlists import ALL_FILTER -from code42cli.cmds.detectionlists import get_choices -from code42cli.cmds.detectionlists import handle_filter_choice -from code42cli.cmds.detectionlists import list_employees -from code42cli.cmds.detectionlists import update_user -from code42cli.cmds.detectionlists.options import cloud_alias_option -from code42cli.cmds.detectionlists.options import notes_option -from code42cli.cmds.detectionlists.options import username_arg -from code42cli.cmds.shared import get_user_id -from code42cli.errors import Code42CLIError -from code42cli.file_readers import read_csv_arg -from code42cli.options import format_option -from code42cli.options import sdk_options -from code42cli.util import deprecation_warning - - -def _get_filter_choices(): - filters = DepartingEmployeeFilters.choices() - return get_choices(filters) - - -DEPRECATION_TEXT = "(DEPRECATED): Use `code42 watchlists` commands instead." - -DATE_FORMAT = "%Y-%m-%d" -filter_option = click.option( - "--filter", - help=f"Departing employee filter options. Defaults to {ALL_FILTER}.", - type=click.Choice(_get_filter_choices()), - default=ALL_FILTER, - callback=lambda ctx, param, arg: handle_filter_choice(arg), -) - - -@click.group( - cls=OrderedGroup, - help=f"{DEPRECATION_TEXT}\n\nAdd and remove employees from the Departing Employees detection list.", -) -@sdk_options(hidden=True) -def departing_employee(state): - pass - - -@departing_employee.command( - "list", - help=f"{DEPRECATION_TEXT}\n\nLists the users on the Departing Employees list.", -) -@sdk_options() -@format_option -@filter_option -def _list(state, format, filter): - deprecation_warning(DEPRECATION_TEXT) - employee_generator = _get_departing_employees(state.sdk, filter) - list_employees( - employee_generator, - format, - {"departureDate": "Departure Date"}, - ) - - -@departing_employee.command( - help=f"{DEPRECATION_TEXT}\n\nAdd a user to the Departing Employees detection list." -) -@username_arg -@click.option( - "--departure-date", - help="The date the employee is departing. Format: yyyy-MM-dd.", - type=click.DateTime(formats=[DATE_FORMAT]), -) -@cloud_alias_option -@notes_option -@sdk_options() -def add(state, username, cloud_alias, departure_date, notes): - - deprecation_warning(DEPRECATION_TEXT) - _add_departing_employee(state.sdk, username, cloud_alias, departure_date, notes) - - -@departing_employee.command( - help=f"{DEPRECATION_TEXT}\n\nRemove a user from the Departing Employees detection list." -) -@username_arg -@sdk_options() -def remove(state, username): - deprecation_warning(DEPRECATION_TEXT) - _remove_departing_employee(state.sdk, username) - - -@departing_employee.group( - cls=OrderedGroup, - help=f"{DEPRECATION_TEXT}\n\nTools for executing bulk departing employee actions.", -) -@sdk_options(hidden=True) -def bulk(state): - pass - - -DEPARTING_EMPLOYEE_CSV_HEADERS = ["username", "cloud_alias", "departure_date", "notes"] - -REMOVE_EMPLOYEE_HEADERS = ["username"] - -departing_employee_generate_template = generate_template_cmd_factory( - group_name="departing_employee", - commands_dict={ - "add": DEPARTING_EMPLOYEE_CSV_HEADERS, - "remove": REMOVE_EMPLOYEE_HEADERS, - }, -) -bulk.add_command(departing_employee_generate_template) - - -@bulk.command( - name="add", - help=f"{DEPRECATION_TEXT}\n\nBulk add users to the departing employees detection list using " - f"a CSV file with format: {','.join(DEPARTING_EMPLOYEE_CSV_HEADERS)}.", -) -@read_csv_arg(headers=DEPARTING_EMPLOYEE_CSV_HEADERS) -@sdk_options() -def bulk_add(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk # Force initialization of py42 to only happen once. - - def handle_row(username, cloud_alias, departure_date, notes): - if departure_date: - try: - departure_date = click.DateTime(formats=[DATE_FORMAT]).convert( - departure_date, None, None - ) - except click.exceptions.BadParameter: - message = ( - f"Invalid date {departure_date}, valid date format {DATE_FORMAT}." - ) - raise Code42CLIError(message) - _add_departing_employee(sdk, username, cloud_alias, departure_date, notes) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Adding users to the Departing Employees detection list:", - ) - - -@bulk.command( - name="remove", - help=f"{DEPRECATION_TEXT}\n\nBulk remove users from the departing employees detection list " - f"using a CSV file with format {','.join(REMOVE_EMPLOYEE_HEADERS)}.", -) -@read_csv_arg(headers=REMOVE_EMPLOYEE_HEADERS) -@sdk_options() -def bulk_remove(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk - - def handle_row(username): - _remove_departing_employee(sdk, username) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Removing users from the Departing Employees detection list:", - ) - - -def _get_departing_employees(sdk, filter): - return sdk.detectionlists.departing_employee.get_all(filter) - - -def _add_departing_employee(sdk, username, cloud_alias, departure_date, notes): - if departure_date: - departure_date = departure_date.strftime(DATE_FORMAT) - user_id = get_user_id(sdk, username) - sdk.detectionlists.departing_employee.add(user_id, departure_date) - update_user(sdk, username, cloud_alias=cloud_alias, notes=notes) - - -def _remove_departing_employee(sdk, username): - user_id = get_user_id(sdk, username) - sdk.detectionlists.departing_employee.remove(user_id) diff --git a/src/code42cli/cmds/detectionlists/__init__.py b/src/code42cli/cmds/detectionlists/__init__.py deleted file mode 100644 index ddc039d4..00000000 --- a/src/code42cli/cmds/detectionlists/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -import click -from py42.services.detectionlists import _DetectionListFilters - -from code42cli.cmds.shared import get_user_id -from code42cli.output_formats import OutputFormat -from code42cli.output_formats import OutputFormatter - - -ALL_FILTER = "ALL" - - -def get_choices(filters): - filters.remove(_DetectionListFilters.OPEN) - filters.append(ALL_FILTER) - return filters - - -def handle_filter_choice(choice): - if choice == ALL_FILTER: - return _DetectionListFilters.OPEN - return choice - - -def list_employees(employee_generator, output_format, additional_header_items=None): - additional_header_items = additional_header_items or {} - header = {"userName": "Username", "notes": "Notes", **additional_header_items} - employee_list = [] - for employees in employee_generator: - for employee in employees["items"]: - if employee.get("notes") and output_format == OutputFormat.TABLE: - employee["notes"] = ( - employee["notes"].replace("\n", "\\n").replace("\t", "\\t") - ) - employee_list.append(employee) - if employee_list: - formatter = OutputFormatter(output_format, header) - formatter.echo_formatted_list(employee_list) - else: - click.echo("No users found.") - - -def update_user(sdk, username, cloud_alias=None, risk_tag=None, notes=None): - """Updates a detection list user. - - Args: - sdk (py42.sdk.SDKClient): py42 sdk. - username (str): The username of the user to update. - cloud_alias (str): A cloud alias to add to the user. - risk_tag (iter[str]): A list of risk tags associated with user. - notes (str): Notes about the user. - """ - user_id = get_user_id(sdk, username) - _update_cloud_alias(sdk, user_id, cloud_alias) - _update_risk_tags(sdk, username, risk_tag) - _update_notes(sdk, user_id, notes) - - -def _update_cloud_alias(sdk, user_id, cloud_alias): - if cloud_alias: - profile = sdk.detectionlists.get_user_by_id(user_id) - cloud_aliases = profile.data.get("cloudUsernames") or [] - for alias in cloud_aliases: - if alias != profile["userName"]: - sdk.detectionlists.remove_user_cloud_alias(user_id, alias) - sdk.detectionlists.add_user_cloud_alias(user_id, cloud_alias) - - -def _update_risk_tags(sdk, username, risk_tag): - if risk_tag: - add_risk_tags(sdk, username, risk_tag) - - -def _update_notes(sdk, user_id, notes): - if notes: - sdk.detectionlists.update_user_notes(user_id, notes) - - -def add_risk_tags(sdk, username, risk_tag): - risk_tag = handle_list_args(risk_tag) - user_id = get_user_id(sdk, username) - sdk.detectionlists.add_user_risk_tags(user_id, risk_tag) - - -def remove_risk_tags(sdk, username, risk_tag): - risk_tag = handle_list_args(risk_tag) - user_id = get_user_id(sdk, username) - sdk.detectionlists.remove_user_risk_tags(user_id, risk_tag) - - -def handle_list_args(list_arg): - """Converts str args to a list. Useful for `bulk` commands which don't use click's argument - parsing but instead pass in values from files, such as in the form "item1 item2".""" - if isinstance(list_arg, str): - return list_arg.split() - return list_arg diff --git a/src/code42cli/cmds/detectionlists/options.py b/src/code42cli/cmds/detectionlists/options.py deleted file mode 100644 index da538e93..00000000 --- a/src/code42cli/cmds/detectionlists/options.py +++ /dev/null @@ -1,11 +0,0 @@ -import click - -username_arg = click.argument("username") -cloud_alias_option = click.option( - "--cloud-alias", - help="If the employee has an email alias other than their Code42 username " - "that they use for cloud services such as Google Drive, OneDrive, or Box, " - "add and monitor the alias. WARNING: Adding a cloud alias will override any " - "existing cloud alias for this user.", -) -notes_option = click.option("--notes", help="Optional notes about the employee.") diff --git a/src/code42cli/cmds/high_risk_employee.py b/src/code42cli/cmds/high_risk_employee.py deleted file mode 100644 index a5153d1b..00000000 --- a/src/code42cli/cmds/high_risk_employee.py +++ /dev/null @@ -1,240 +0,0 @@ -import click -from py42.clients.detectionlists import RiskTags -from py42.services.detectionlists.high_risk_employee import HighRiskEmployeeFilters - -from code42cli.bulk import generate_template_cmd_factory -from code42cli.bulk import run_bulk_process -from code42cli.click_ext.groups import OrderedGroup -from code42cli.cmds.detectionlists import add_risk_tags as _add_risk_tags -from code42cli.cmds.detectionlists import ALL_FILTER -from code42cli.cmds.detectionlists import get_choices -from code42cli.cmds.detectionlists import handle_filter_choice -from code42cli.cmds.detectionlists import handle_list_args -from code42cli.cmds.detectionlists import list_employees -from code42cli.cmds.detectionlists import remove_risk_tags as _remove_risk_tags -from code42cli.cmds.detectionlists import update_user -from code42cli.cmds.detectionlists.options import cloud_alias_option -from code42cli.cmds.detectionlists.options import notes_option -from code42cli.cmds.detectionlists.options import username_arg -from code42cli.cmds.shared import get_user_id -from code42cli.file_readers import read_csv_arg -from code42cli.options import format_option -from code42cli.options import sdk_options -from code42cli.util import deprecation_warning - -DEPRECATION_TEXT = "(DEPRECATED): Use `code42 watchlists` commands instead." - - -def _get_filter_choices(): - filters = HighRiskEmployeeFilters.choices() - return get_choices(filters) - - -filter_option = click.option( - "--filter", - help=f"High risk employee filter options. Defaults to {ALL_FILTER}.", - type=click.Choice(_get_filter_choices()), - default=ALL_FILTER, - callback=lambda ctx, param, arg: handle_filter_choice(arg), -) - - -risk_tag_option = click.option( - "-t", - "--risk-tag", - multiple=True, - type=click.Choice(RiskTags.choices()), - help="Risk tags associated with the employee.", -) - - -@click.group( - cls=OrderedGroup, - help=f"{DEPRECATION_TEXT}\n\nAdd and remove employees from the High Risk Employees detection list.", -) -@sdk_options(hidden=True) -def high_risk_employee(state): - pass - - -@high_risk_employee.command( - "list", - help=f"{DEPRECATION_TEXT}\n\nLists the employees on the High Risk Employee list.", -) -@sdk_options() -@format_option -@filter_option -def _list(state, format, filter): - deprecation_warning(DEPRECATION_TEXT) - employee_generator = _get_high_risk_employees(state.sdk, filter) - list_employees(employee_generator, format) - - -@high_risk_employee.command( - help=f"{DEPRECATION_TEXT}\n\nAdd a user to the high risk employees detection list." -) -@cloud_alias_option -@notes_option -@risk_tag_option -@username_arg -@sdk_options() -def add(state, username, cloud_alias, risk_tag, notes): - deprecation_warning(DEPRECATION_TEXT) - _add_high_risk_employee(state.sdk, username, cloud_alias, risk_tag, notes) - - -@high_risk_employee.command( - help=f"{DEPRECATION_TEXT}\n\nRemove a user from the high risk employees detection list." -) -@username_arg -@sdk_options() -def remove(state, username): - deprecation_warning(DEPRECATION_TEXT) - _remove_high_risk_employee(state.sdk, username) - - -@high_risk_employee.command( - help=f"{DEPRECATION_TEXT}\n\nAssociates risk tags with a user." -) -@username_arg -@risk_tag_option -@sdk_options() -def add_risk_tags(state, username, risk_tag): - deprecation_warning(DEPRECATION_TEXT) - _add_risk_tags(state.sdk, username, risk_tag) - - -@high_risk_employee.command( - help=f"{DEPRECATION_TEXT}\n\nDisassociates risk tags from a user." -) -@username_arg -@risk_tag_option -@sdk_options() -def remove_risk_tags(state, username, risk_tag): - deprecation_warning(DEPRECATION_TEXT) - _remove_risk_tags(state.sdk, username, risk_tag) - - -@high_risk_employee.group( - cls=OrderedGroup, - help=f"{DEPRECATION_TEXT}\n\nTools for executing high risk employee actions in bulk.", -) -@sdk_options(hidden=True) -def bulk(state): - pass - - -HIGH_RISK_EMPLOYEE_CSV_HEADERS = ["username", "cloud_alias", "risk_tag", "notes"] -RISK_TAG_CSV_HEADERS = ["username", "tag"] -REMOVE_EMPLOYEE_HEADERS = ["username"] - -high_risk_employee_generate_template = generate_template_cmd_factory( - group_name="high_risk_employee", - commands_dict={ - "add": HIGH_RISK_EMPLOYEE_CSV_HEADERS, - "remove": REMOVE_EMPLOYEE_HEADERS, - "add-risk-tags": RISK_TAG_CSV_HEADERS, - "remove-risk-tags": RISK_TAG_CSV_HEADERS, - }, -) -bulk.add_command(high_risk_employee_generate_template) - - -@bulk.command( - name="add", - help=f"{DEPRECATION_TEXT}\n\nBulk add users to the high risk employees detection list using a " - f"CSV file with format: {','.join(HIGH_RISK_EMPLOYEE_CSV_HEADERS)}.", -) -@read_csv_arg(headers=HIGH_RISK_EMPLOYEE_CSV_HEADERS) -@sdk_options() -def bulk_add(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk - - def handle_row(username, cloud_alias, risk_tag, notes): - _add_high_risk_employee(sdk, username, cloud_alias, risk_tag, notes) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Adding users to high risk employee detection list:", - ) - - -@bulk.command( - name="remove", - help=f"{DEPRECATION_TEXT}\n\nBulk remove users from the high risk employees detection list " - f"using a CSV file with format {','.join(REMOVE_EMPLOYEE_HEADERS)}.", -) -@read_csv_arg(headers=REMOVE_EMPLOYEE_HEADERS) -@sdk_options() -def bulk_remove(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk - - def handle_row(username): - _remove_high_risk_employee(sdk, username) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Removing users from high risk employee detection list:", - ) - - -@bulk.command( - name="add-risk-tags", - help=f"{DEPRECATION_TEXT}\n\nAdds risk tags to users in bulk using a CSV file with format: " - f"{','.join(RISK_TAG_CSV_HEADERS)}.", -) -@read_csv_arg(headers=RISK_TAG_CSV_HEADERS) -@sdk_options() -def bulk_add_risk_tags(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk - - def handle_row(username, tag): - _add_risk_tags(sdk, username, tag) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Adding risk tags to users:", - ) - - -@bulk.command( - name="remove-risk-tags", - help=f"{DEPRECATION_TEXT}\n\nRemoves risk tags from users in bulk using a CSV file with " - f"format: {','.join(RISK_TAG_CSV_HEADERS)}.", -) -@read_csv_arg(headers=RISK_TAG_CSV_HEADERS) -@sdk_options() -def bulk_remove_risk_tags(state, csv_rows): - deprecation_warning(DEPRECATION_TEXT) - sdk = state.sdk - - def handle_row(username, tag): - _remove_risk_tags(sdk, username, tag) - - run_bulk_process( - handle_row, - csv_rows, - progress_label="Removing risk tags from users:", - ) - - -def _get_high_risk_employees(sdk, filter): - return sdk.detectionlists.high_risk_employee.get_all(filter) - - -def _add_high_risk_employee(sdk, username, cloud_alias, risk_tag, notes): - risk_tag = handle_list_args(risk_tag) - user_id = get_user_id(sdk, username) - sdk.detectionlists.high_risk_employee.add(user_id) - update_user(sdk, username, cloud_alias=cloud_alias, risk_tag=risk_tag, notes=notes) - - -def _remove_high_risk_employee(sdk, username): - user_id = get_user_id(sdk, username) - sdk.detectionlists.high_risk_employee.remove(user_id) diff --git a/src/code42cli/cmds/securitydata.py b/src/code42cli/cmds/securitydata.py index 0a0a2b77..eae94d3f 100644 --- a/src/code42cli/cmds/securitydata.py +++ b/src/code42cli/cmds/securitydata.py @@ -40,10 +40,11 @@ logger = get_main_cli_logger() MAX_EVENT_PAGE_SIZE = 10000 -DEPRECATION_TEXT = "(DEPRECATED): V1 file events are deprecated. Update your profile with `code42 profile update --use-v2-file-events True` to use the new V2 file event data model." SECURITY_DATA_KEYWORD = "file events" +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." + def exposure_type_callback(): def callback(ctx, param, arg): @@ -375,7 +376,8 @@ def file_event_options(f): @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def security_data(state): - """Get and send file event data.""" + """DEPRECATED - Get and send file event data.""" + deprecation_warning(DEPRECATION_TEXT) # store cursor getter on the group state so shared --begin option can use it in validation state.cursor_getter = _get_file_event_cursor_store @@ -410,9 +412,6 @@ def search( ): """Search for file events.""" - if state.profile.use_v2_file_events != "True": - deprecation_warning(DEPRECATION_TEXT) - if format == FileEventsOutputFormat.CEF and columns: raise click.BadOptionUsage( "columns", "--columns option can't be used with CEF format." diff --git a/src/code42cli/cmds/shared.py b/src/code42cli/cmds/shared.py index 4fa7a29b..e87da7d8 100644 --- a/src/code42cli/cmds/shared.py +++ b/src/code42cli/cmds/shared.py @@ -5,7 +5,7 @@ @lru_cache(maxsize=None) def get_user_id(sdk, username): - """Returns the user's UID (referred to by `user_id` in detection lists). + """Returns the user's UID. Raises `UserDoesNotExistError` if the user doesn't exist in the Code42 server. Args: diff --git a/src/code42cli/cmds/trustedactivities.py b/src/code42cli/cmds/trustedactivities.py index 342772d2..95f3c477 100644 --- a/src/code42cli/cmds/trustedactivities.py +++ b/src/code42cli/cmds/trustedactivities.py @@ -9,6 +9,9 @@ from code42cli.options import format_option from code42cli.options import sdk_options from code42cli.output_formats import OutputFormatter +from code42cli.util import deprecation_warning + +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." resource_id_arg = click.argument("resource-id", type=int) type_option = click.option( @@ -40,7 +43,8 @@ def _get_trust_header(): @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def trusted_activities(state): - """Manage trusted activities and resources.""" + """DEPRECATED - Manage trusted activities and resources.""" + deprecation_warning(DEPRECATION_TEXT) pass diff --git a/src/code42cli/cmds/watchlists.py b/src/code42cli/cmds/watchlists.py index f4b0c7b6..4c6835e9 100644 --- a/src/code42cli/cmds/watchlists.py +++ b/src/code42cli/cmds/watchlists.py @@ -15,12 +15,16 @@ from code42cli.options import format_option from code42cli.options import sdk_options from code42cli.output_formats import DataFrameOutputFormatter +from code42cli.util import deprecation_warning + +DEPRECATION_TEXT = "Incydr functionality is deprecated. Use the Incydr CLI instead (https://developer.code42.com/)." @click.group(cls=OrderedGroup) @sdk_options(hidden=True) def watchlists(state): - """Manage watchlist user memberships.""" + """DEPRECATED - Manage watchlist user memberships.""" + deprecation_warning(DEPRECATION_TEXT) pass diff --git a/src/code42cli/main.py b/src/code42cli/main.py index 5427c653..519d2d21 100644 --- a/src/code42cli/main.py +++ b/src/code42cli/main.py @@ -7,18 +7,17 @@ import click from click_plugins import with_plugins from pkg_resources import iter_entry_points -from py42.settings import set_user_agent_suffix +from py42.settings import set_user_agent_prefix from code42cli import BANNER from code42cli import PRODUCT_NAME +from code42cli.__version__ import __version__ from code42cli.click_ext.groups import ExceptionHandlingGroup from code42cli.cmds.alert_rules import alert_rules from code42cli.cmds.alerts import alerts from code42cli.cmds.auditlogs import audit_logs from code42cli.cmds.cases import cases -from code42cli.cmds.departing_employee import departing_employee from code42cli.cmds.devices import devices -from code42cli.cmds.high_risk_employee import high_risk_employee from code42cli.cmds.legal_hold import legal_hold from code42cli.cmds.profile import profile from code42cli.cmds.securitydata import security_data @@ -41,7 +40,7 @@ def exit_on_interrupt(signal, frame): # Sets part of the user agent string that py42 attaches to requests for the purposes of # identifying CLI users. -set_user_agent_suffix(PRODUCT_NAME) +set_user_agent_prefix(f"{PRODUCT_NAME}/{__version__} (Code42; code42.com )") CONTEXT_SETTINGS = { "help_option_names": ["-h", "--help"], @@ -88,9 +87,7 @@ def cli(state, python, script_dir): cli.add_command(alert_rules) cli.add_command(audit_logs) cli.add_command(cases) -cli.add_command(departing_employee) cli.add_command(devices) -cli.add_command(high_risk_employee) cli.add_command(legal_hold) cli.add_command(profile) cli.add_command(security_data) diff --git a/src/code42cli/output_formats.py b/src/code42cli/output_formats.py index b0f87a25..2b2ab51f 100644 --- a/src/code42cli/output_formats.py +++ b/src/code42cli/output_formats.py @@ -16,7 +16,6 @@ from code42cli.util import find_format_width from code42cli.util import format_to_table - CEF_DEFAULT_PRODUCT_NAME = "Advanced Exfiltration Detection" CEF_DEFAULT_SEVERITY_LEVEL = "5" @@ -90,6 +89,8 @@ def _iter_table(self, dfs, columns=None, **kwargs): if df.empty: return # convert everything to strings so we can left-justify format + # applymap() is deprecated in favor of map() for pandas 2.0+ (method renamed) + # pandas only supports Python 3.8+, update this once we drop support for Python 3.7 df = df.fillna("").applymap(str) # set overrideable default kwargs kwargs = { diff --git a/tests/cmds/conftest.py b/tests/cmds/conftest.py index 3dd9fea5..c60fdbf3 100644 --- a/tests/cmds/conftest.py +++ b/tests/cmds/conftest.py @@ -3,7 +3,6 @@ import threading import pytest -from py42.exceptions import Py42UserAlreadyAddedError from py42.exceptions import Py42UserNotOnListError from py42.sdk import SDKClient from requests import HTTPError @@ -81,11 +80,6 @@ def custom_error(mocker): return err -@pytest.fixture -def user_already_added_error(custom_error): - return Py42UserAlreadyAddedError(custom_error, TEST_ID, "detection list") - - def get_filter_value_from_json(json, filter_index): return json_module.loads(str(json))["filters"][filter_index]["value"] diff --git a/tests/cmds/detectionlists/__init__.py b/tests/cmds/detectionlists/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/cmds/detectionlists/test_init.py b/tests/cmds/detectionlists/test_init.py deleted file mode 100644 index c07f3c40..00000000 --- a/tests/cmds/detectionlists/test_init.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest -from tests.conftest import create_mock_response - -from code42cli.cmds.detectionlists import update_user - -MOCK_USER_ID = "USER-ID" -MOCK_USER_NAME = "test@example.com" -MOCK_ALIAS = "alias@example" -MOCK_USER_PROFILE_RESPONSE = f""" -{{ - "type$": "USER_V2", - "tenantId": "TENANT-ID", - "userId": "{MOCK_USER_ID}", - "userName": "{MOCK_USER_NAME}", - "displayName": "Test", - "notes": "Notes", - "cloudUsernames": ["{MOCK_ALIAS}", "{MOCK_USER_NAME}"], - "riskFactors": ["HIGH_IMPACT_EMPLOYEE"] -}} -""" - - -@pytest.fixture -def user_response_with_cloud_aliases(mocker): - return create_mock_response(mocker, data=MOCK_USER_PROFILE_RESPONSE) - - -@pytest.fixture -def mock_user_id(mocker): - mock = mocker.patch("code42cli.cmds.detectionlists.get_user_id") - mock.return_value = MOCK_USER_ID - return mock - - -def test_update_user_when_given_cloud_alias_add_cloud_alias( - sdk, user_response_with_cloud_aliases, mock_user_id -): - sdk.detectionlists.get_user_by_id.return_value = user_response_with_cloud_aliases - update_user(sdk, MOCK_USER_NAME, cloud_alias="new.alias@exaple.com") - sdk.detectionlists.add_user_cloud_alias.assert_called_once_with( - MOCK_USER_ID, "new.alias@exaple.com" - ) - - -def test_update_user_when_given_cloud_alias_first_removes_old_alias( - sdk, user_response_with_cloud_aliases, mock_user_id -): - sdk.detectionlists.get_user_by_id.return_value = user_response_with_cloud_aliases - update_user(sdk, MOCK_USER_NAME, cloud_alias="new.alias@exaple.com") - sdk.detectionlists.remove_user_cloud_alias.assert_called_once_with( - MOCK_USER_ID, MOCK_ALIAS - ) diff --git a/tests/cmds/test_auditlogs.py b/tests/cmds/test_auditlogs.py index 8567fa1e..8faf50ae 100644 --- a/tests/cmds/test_auditlogs.py +++ b/tests/cmds/test_auditlogs.py @@ -619,30 +619,30 @@ def test_search_if_error_occurs_when_processing_event_timestamp_does_not_store_e ) -def test_search_when_table_format_and_using_output_via_pager_only_includes_header_keys_once( - cli_state, - runner, - mock_audit_log_response_with_10_records, - audit_log_cursor_with_checkpoint, -): - cli_state.sdk.auditlogs.get_all.return_value = ( - mock_audit_log_response_with_10_records - ) - result = runner.invoke( - cli, - ["audit-logs", "search", "--use-checkpoint", "test"], - obj=cli_state, - ) - output = result.output - output = output.split(" ") - output = [s for s in output if s] - assert ( - output.count("Timestamp") - == output.count("ActorName") - == output.count("ActorIpAddress") - == output.count("AffectedUserUID") - == 1 - ) +# def test_search_when_table_format_and_using_output_via_pager_only_includes_header_keys_once( +# cli_state, +# runner, +# mock_audit_log_response_with_10_records, +# audit_log_cursor_with_checkpoint, +# ): +# cli_state.sdk.auditlogs.get_all.return_value = ( +# mock_audit_log_response_with_10_records +# ) +# result = runner.invoke( +# cli, +# ["audit-logs", "search", "--use-checkpoint", "test"], +# obj=cli_state, +# ) +# output = result.output +# output = output.split(" ") +# output = [s for s in output if s] +# assert ( +# output.count("Timestamp") +# == output.count("ActorName") +# == output.count("ActorIpAddress") +# == output.count("AffectedUserUID") +# == 1 +# ) def test_send_to_if_error_occurs_still_processes_events( diff --git a/tests/cmds/test_departing_employee.py b/tests/cmds/test_departing_employee.py deleted file mode 100644 index d682709b..00000000 --- a/tests/cmds/test_departing_employee.py +++ /dev/null @@ -1,441 +0,0 @@ -import json - -import pytest -from py42.services.detectionlists.departing_employee import DepartingEmployeeFilters -from tests.cmds.conftest import get_generator_for_get_all -from tests.cmds.conftest import get_user_not_on_list_side_effect -from tests.cmds.conftest import thread_safe_side_effect -from tests.conftest import TEST_ID - -from .conftest import TEST_EMPLOYEE -from code42cli.main import cli - - -DEPARTING_EMPLOYEE_ITEM = """{ - "type$": "DEPARTING_EMPLOYEE_V2", - "tenantId": "1111111-af5b-4231-9d8e-000000000", - "userId": "TEST USER UID", - "userName": "test.testerson@example.com", - "displayName": "Testerson", - "notes": "Leaving for competitor", - "createdAt": "2020-06-23T19:57:37.1345130Z", - "status": "OPEN", - "cloudUsernames": ["cloud@example.com"], - "departureDate": "2020-07-07" -} -""" -DEPARTING_EMPLOYEE_COMMAND = "departing-employee" - - -@pytest.fixture() -def mock_get_all_empty_state(mocker, cli_state_with_user): - generator = get_generator_for_get_all(mocker, None) - cli_state_with_user.sdk.detectionlists.departing_employee.get_all.side_effect = ( - generator - ) - return cli_state_with_user - - -@pytest.fixture() -def mock_get_all_state(mocker, cli_state_with_user): - generator = get_generator_for_get_all(mocker, DEPARTING_EMPLOYEE_ITEM) - cli_state_with_user.sdk.detectionlists.departing_employee.get_all.side_effect = ( - generator - ) - return cli_state_with_user - - -def test_list_departing_employees_lists_expected_properties(runner, mock_get_all_state): - res = runner.invoke(cli, ["departing-employee", "list"], obj=mock_get_all_state) - assert "Username" in res.output - assert "Notes" in res.output - assert "test.testerson@example.com" in res.output - assert "Leaving for competitor" in res.output - assert "Departure Date" in res.output - assert "2020-07-07" in res.output - - -def test_list_departing_employees_converts_all_to_open(runner, mock_get_all_state): - runner.invoke( - cli, ["departing-employee", "list", "--filter", "ALL"], obj=mock_get_all_state - ) - mock_get_all_state.sdk.detectionlists.departing_employee.get_all.assert_called_once_with( - DepartingEmployeeFilters.OPEN - ) - - -def test_list_departing_employees_when_given_raw_json_lists_expected_properties( - runner, mock_get_all_state -): - res = runner.invoke( - cli, ["departing-employee", "list", "-f", "RAW-JSON"], obj=mock_get_all_state - ) - assert "userName" in res.output - assert "notes" in res.output - assert "test.testerson@example.com" in res.output - assert "Leaving for competitor" in res.output - assert "cloudUsernames" in res.output - assert "cloud@example.com" in res.output - assert "departureDate" in res.output - assert "2020-07-07" in res.output - - -def test_list_departing_employees_when_no_employees_echos_expected_message( - runner, mock_get_all_empty_state -): - res = runner.invoke( - cli, ["departing-employee", "list"], obj=mock_get_all_empty_state - ) - assert "No users found." in res.output - - -def test_list_departing_employees_when_table_format_and_notes_contains_newlines_escapes_them( - runner, mocker, cli_state_with_user -): - new_line_text = str(DEPARTING_EMPLOYEE_ITEM).replace( - "Leaving for competitor", r"Line1\nLine2" - ) - generator = get_generator_for_get_all(mocker, new_line_text) - cli_state_with_user.sdk.detectionlists.departing_employee.get_all.side_effect = ( - generator - ) - res = runner.invoke(cli, ["departing-employee", "list"], obj=cli_state_with_user) - assert "Line1\\nLine2" in res.output - - -def test_list_departing_employees_uses_filter_option(runner, mock_get_all_state): - runner.invoke( - cli, - [ - "departing-employee", - "list", - "--filter", - DepartingEmployeeFilters.EXFILTRATION_30_DAYS, - ], - obj=mock_get_all_state, - ) - mock_get_all_state.sdk.detectionlists.departing_employee.get_all.assert_called_once_with( - DepartingEmployeeFilters.EXFILTRATION_30_DAYS - ) - - -def test_list_departing_employees_handles_employees_with_no_notes( - runner, mocker, cli_state_with_user -): - hr_json = json.loads(DEPARTING_EMPLOYEE_ITEM) - hr_json["notes"] = None - new_text = json.dumps(hr_json) - generator = get_generator_for_get_all(mocker, new_text) - cli_state_with_user.sdk.detectionlists.departing_employee.get_all.side_effect = ( - generator - ) - res = runner.invoke(cli, ["departing-employee", "list"], obj=cli_state_with_user) - assert "None" in res.output - - -def test_add_departing_employee_when_given_cloud_alias_adds_alias( - runner, cli_state_with_user -): - alias = "departing employee alias" - runner.invoke( - cli, - ["departing-employee", "add", TEST_EMPLOYEE, "--cloud-alias", alias], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.add_user_cloud_alias.assert_called_once_with( - TEST_ID, alias - ) - - -def test_add_departing_employee_when_given_notes_updates_notes( - runner, cli_state_with_user, profile -): - notes = "is leaving" - runner.invoke( - cli, - ["departing-employee", "add", TEST_EMPLOYEE, "--notes", notes], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.update_user_notes.assert_called_once_with( - TEST_ID, notes - ) - - -def test_add_departing_employee_adds( - runner, - cli_state_with_user, -): - departure_date = "2020-02-02" - runner.invoke( - cli, - [ - "departing-employee", - "add", - TEST_EMPLOYEE, - "--departure-date", - departure_date, - ], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.departing_employee.add.assert_called_once_with( - TEST_ID, "2020-02-02" - ) - - -def test_add_departing_employee_when_user_does_not_exist_exits( - runner, cli_state_without_user -): - result = runner.invoke( - cli, ["departing-employee", "add", TEST_EMPLOYEE], obj=cli_state_without_user - ) - assert result.exit_code == 1 - assert ( - f"User '{TEST_EMPLOYEE}' does not exist or you do not have permission to view them." - in result.output - ) - - -def test_add_departing_employee_when_user_already_exits_with_correct_message( - runner, cli_state_with_user, user_already_added_error -): - def add_user(user): - raise user_already_added_error - - cli_state_with_user.sdk.detectionlists.departing_employee.add.side_effect = add_user - result = runner.invoke( - cli, ["departing-employee", "add", TEST_EMPLOYEE], obj=cli_state_with_user - ) - assert result.exit_code == 1 - assert f"'{TEST_EMPLOYEE}' is already on the departing-employee list." - - -def test_remove_departing_employee_calls_remove(runner, cli_state_with_user): - runner.invoke( - cli, ["departing-employee", "remove", TEST_EMPLOYEE], obj=cli_state_with_user - ) - cli_state_with_user.sdk.detectionlists.departing_employee.remove.assert_called_once_with( - TEST_ID - ) - - -def test_remove_departing_employee_when_user_does_not_exist_exits( - runner, cli_state_without_user -): - result = runner.invoke( - cli, ["departing-employee", "remove", TEST_EMPLOYEE], obj=cli_state_without_user - ) - assert result.exit_code == 1 - assert ( - f"User '{TEST_EMPLOYEE}' does not exist or you do not have permission to view them." - in result.output - ) - - -def test_add_bulk_users_calls_expected_py42_methods(runner, cli_state): - de_add_user = thread_safe_side_effect() - add_user_cloud_alias = thread_safe_side_effect() - update_user_notes = thread_safe_side_effect() - - cli_state.sdk.detectionlists.departing_employee.add.side_effect = de_add_user - cli_state.sdk.detectionlists.add_user_cloud_alias.side_effect = add_user_cloud_alias - cli_state.sdk.detectionlists.update_user_notes.side_effect = update_user_notes - - with runner.isolated_filesystem(): - with open("test_add.csv", "w") as csv: - csv.writelines( - [ - "username,cloud_alias,departure_date,notes\n", - "test_user,test_alias,2020-01-01,test_note\n", - "test_user_2,test_alias_2,2020-02-01,test_note_2\n", - "test_user_3,,,\n", - "test_user_3,,2020-30-02,\n", - "test_user_3,,20-02-2020,\n", - ] - ) - runner.invoke( - cli, ["departing-employee", "bulk", "add", "test_add.csv"], obj=cli_state - ) - de_add_user_call_args = [call[1] for call in de_add_user.call_args_list] - assert de_add_user.call_count == 3 - assert "2020-01-01" in de_add_user_call_args - assert "2020-02-01" in de_add_user_call_args - assert None in de_add_user_call_args - - add_user_cloud_alias_call_args = [ - call[1] for call in add_user_cloud_alias.call_args_list - ] - assert add_user_cloud_alias.call_count == 2 - assert "test_alias" in add_user_cloud_alias_call_args - assert "test_alias_2" in add_user_cloud_alias_call_args - - update_user_notes_call_args = [call[1] for call in update_user_notes.call_args_list] - assert update_user_notes.call_count == 2 - assert "test_note" in update_user_notes_call_args - assert "test_note_2" in update_user_notes_call_args - - -def test_remove_bulk_users_uses_expected_arguments(runner, mocker, cli_state_with_user): - bulk_processor = mocker.patch("code42cli.cmds.departing_employee.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines(["username\n", "test_user1\n", "test_user2\n"]) - runner.invoke( - cli, - ["departing-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state_with_user, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test_user1"}, - {"username": "test_user2"}, - ] - - -def test_remove_bulk_users_uses_expected_arguments_when_no_header( - runner, mocker, cli_state_with_user -): - bulk_processor = mocker.patch("code42cli.cmds.departing_employee.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines(["test_user1\n", "test_user2\n"]) - runner.invoke( - cli, - ["departing-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state_with_user, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test_user1"}, - {"username": "test_user2"}, - ] - - -def test_remove_bulk_users_uses_expected_arguments_when_extra_columns( - runner, mocker, cli_state_with_user -): - bulk_processor = mocker.patch("code42cli.cmds.departing_employee.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines( - [ - "username,test_column\n", - "test_user1,test_value1\n", - "test_user2,test_value2\n", - ] - ) - runner.invoke( - cli, - ["departing-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state_with_user, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test_user1"}, - {"username": "test_user2"}, - ] - - -def test_remove_bulk_users_uses_expected_arguments_when_flat_file( - runner, mocker, cli_state_with_user -): - bulk_processor = mocker.patch("code42cli.cmds.departing_employee.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.txt", "w") as csv: - csv.writelines(["# username\n", "test_user1\n", "test_user2\n"]) - runner.invoke( - cli, - ["departing-employee", "bulk", "remove", "test_remove.txt"], - obj=cli_state_with_user, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test_user1"}, - {"username": "test_user2"}, - ] - - -def test_add_departing_employee_when_invalid_date_validation_raises_error( - runner, cli_state_with_user -): - # day is out of range for month - departure_date = "2020-02-30" - result = runner.invoke( - cli, - [ - "departing-employee", - "add", - TEST_EMPLOYEE, - "--departure-date", - departure_date, - ], - obj=cli_state_with_user, - ) - assert result.exit_code == 2 - assert ( - "Invalid value for '--departure-date': '2020-02-30' does not match the format '%Y-%m-%d'" - in result.output # invalid datetime format - ) or ( - "Invalid value for '--departure-date': invalid datetime format" in result.output - ) - - -def test_add_departing_employee_when_invalid_date_format_validation_raises_error( - runner, cli_state_with_user -): - departure_date = "2020-30-01" - result = runner.invoke( - cli, - [ - "departing-employee", - "add", - TEST_EMPLOYEE, - "--departure-date", - departure_date, - ], - obj=cli_state_with_user, - ) - assert result.exit_code == 2 - assert ( - "Invalid value for '--departure-date': '2020-30-01' does not match the format '%Y-%m-%d'" - in result.output - ) or ( - "Invalid value for '--departure-date': invalid datetime format" in result.output - ) - - -def test_remove_departing_employee_when_user_not_on_list_prints_expected_error( - mocker, runner, cli_state -): - cli_state.sdk.detectionlists.departing_employee.remove.side_effect = ( - get_user_not_on_list_side_effect(mocker, "departing-employee") - ) - test_username = "test@example.com" - result = runner.invoke( - cli, ["departing-employee", "remove", test_username], obj=cli_state - ) - assert ( - f"User with ID '{TEST_ID}' is not currently on the departing-employee list." - in result.output - ) - - -@pytest.mark.parametrize( - "command, error_msg", - [ - (f"{DEPARTING_EMPLOYEE_COMMAND} add", "Missing argument 'USERNAME'."), - ( - f"{DEPARTING_EMPLOYEE_COMMAND} remove", - "Missing argument 'USERNAME'.", - ), - ( - f"{DEPARTING_EMPLOYEE_COMMAND} bulk add", - "Missing argument 'CSV_FILE'.", - ), - ( - f"{DEPARTING_EMPLOYEE_COMMAND} bulk remove", - "Missing argument 'CSV_FILE'.", - ), - ], -) -def test_departing_employee_command_when_missing_required_parameters_returns_error( - command, error_msg, cli_state, runner -): - result = runner.invoke(cli, command.split(" "), obj=cli_state) - assert result.exit_code == 2 - assert error_msg in "".join(result.output) diff --git a/tests/cmds/test_high_risk_employee.py b/tests/cmds/test_high_risk_employee.py deleted file mode 100644 index 136e9239..00000000 --- a/tests/cmds/test_high_risk_employee.py +++ /dev/null @@ -1,440 +0,0 @@ -import json - -import pytest -from py42.services.detectionlists.high_risk_employee import HighRiskEmployeeFilters -from tests.cmds.conftest import get_generator_for_get_all -from tests.cmds.conftest import get_user_not_on_list_side_effect -from tests.cmds.conftest import TEST_EMPLOYEE -from tests.cmds.conftest import thread_safe_side_effect -from tests.conftest import TEST_ID - -from code42cli.main import cli - -_NAMESPACE = "code42cli.cmds.high_risk_employee" - - -HIGH_RISK_EMPLOYEE_ITEM = """{ - "type$": "HIGH_RISK_EMPLOYEE_V2", - "tenantId": "1111111-af5b-4231-9d8e-000000000", - "userId": "TEST USER UID", - "userName": "test.testerson@example.com", - "displayName": "Testerson", - "notes": "Leaving for competitor", - "createdAt": "2020-06-23T19:57:37.1345130Z", - "status": "OPEN", - "cloudUsernames": ["cloud@example.com"], - "riskFactors": ["PERFORMANCE_CONCERNS"] -} -""" -HR_EMPLOYEE_COMMAND = "high-risk-employee" - - -@pytest.fixture() -def mock_get_all_empty_state(mocker, cli_state_with_user): - generator = get_generator_for_get_all(mocker, None) - cli_state_with_user.sdk.detectionlists.high_risk_employee.get_all.side_effect = ( - generator - ) - return cli_state_with_user - - -@pytest.fixture() -def mock_get_all_state(mocker, cli_state_with_user): - generator = get_generator_for_get_all(mocker, HIGH_RISK_EMPLOYEE_ITEM) - cli_state_with_user.sdk.detectionlists.high_risk_employee.get_all.side_effect = ( - generator - ) - return cli_state_with_user - - -def test_list_high_risk_employees_lists_expected_properties(runner, mock_get_all_state): - res = runner.invoke(cli, ["high-risk-employee", "list"], obj=mock_get_all_state) - assert "Username" in res.output - assert "Notes" in res.output - assert "test.testerson@example.com" in res.output - - -def test_list_high_risk_employees_converts_all_to_open(runner, mock_get_all_state): - runner.invoke( - cli, ["high-risk-employee", "list", "--filter", "ALL"], obj=mock_get_all_state - ) - mock_get_all_state.sdk.detectionlists.high_risk_employee.get_all.assert_called_once_with( - HighRiskEmployeeFilters.OPEN - ) - - -def test_list_high_risk_employees_when_given_raw_json_lists_expected_properties( - runner, mock_get_all_state -): - res = runner.invoke( - cli, ["high-risk-employee", "list", "-f", "RAW-JSON"], obj=mock_get_all_state - ) - assert "userName" in res.output - assert "notes" in res.output - assert "test.testerson@example.com" in res.output - assert "Leaving for competitor" in res.output - assert "cloudUsernames" in res.output - assert "cloud@example.com" in res.output - assert "riskFactors" in res.output - assert "PERFORMANCE_CONCERNS" in res.output - - -def test_list_high_risk_employees_when_no_employees_echos_expected_message( - runner, mock_get_all_empty_state -): - res = runner.invoke( - cli, ["high-risk-employee", "list"], obj=mock_get_all_empty_state - ) - assert "No users found." in res.output - - -def test_list_high_risk_employees_uses_filter_option(runner, mock_get_all_state): - runner.invoke( - cli, - [ - "high-risk-employee", - "list", - "--filter", - HighRiskEmployeeFilters.EXFILTRATION_30_DAYS, - ], - obj=mock_get_all_state, - ) - mock_get_all_state.sdk.detectionlists.high_risk_employee.get_all.assert_called_once_with( - HighRiskEmployeeFilters.EXFILTRATION_30_DAYS, - ) - - -def test_list_high_risk_employees_when_table_format_and_notes_contains_newlines_escapes_them( - runner, mocker, cli_state_with_user -): - new_line_text = str(HIGH_RISK_EMPLOYEE_ITEM).replace( - "Leaving for competitor", r"Line1\nLine2" - ) - generator = get_generator_for_get_all(mocker, new_line_text) - cli_state_with_user.sdk.detectionlists.high_risk_employee.get_all.side_effect = ( - generator - ) - res = runner.invoke(cli, ["high-risk-employee", "list"], obj=cli_state_with_user) - assert "Line1\\nLine2" in res.output - - -def test_list_high_risk_employees_handles_employees_with_no_notes( - runner, mocker, cli_state_with_user -): - hr_json = json.loads(HIGH_RISK_EMPLOYEE_ITEM) - hr_json["notes"] = None - new_text = json.dumps(hr_json) - generator = get_generator_for_get_all(mocker, new_text) - cli_state_with_user.sdk.detectionlists.high_risk_employee.get_all.side_effect = ( - generator - ) - res = runner.invoke(cli, ["high-risk-employee", "list"], obj=cli_state_with_user) - assert "None" in res.output - - -def test_add_high_risk_employee_adds(runner, cli_state_with_user): - runner.invoke( - cli, ["high-risk-employee", "add", TEST_EMPLOYEE], obj=cli_state_with_user - ) - cli_state_with_user.sdk.detectionlists.high_risk_employee.add.assert_called_once_with( - TEST_ID - ) - - -def test_add_high_risk_employee_when_given_cloud_alias_adds_alias( - runner, cli_state_with_user -): - alias = "risk employee alias" - runner.invoke( - cli, - ["high-risk-employee", "add", TEST_EMPLOYEE, "--cloud-alias", alias], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.add_user_cloud_alias.assert_called_once_with( - TEST_ID, alias - ) - - -def test_add_high_risk_employee_when_given_risk_tags_adds_tags( - runner, cli_state_with_user -): - runner.invoke( - cli, - [ - "high-risk-employee", - "add", - TEST_EMPLOYEE, - "-t", - "FLIGHT_RISK", - "-t", - "ELEVATED_ACCESS_PRIVILEGES", - "-t", - "POOR_SECURITY_PRACTICES", - ], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.add_user_risk_tags.assert_called_once_with( - TEST_ID, - ("FLIGHT_RISK", "ELEVATED_ACCESS_PRIVILEGES", "POOR_SECURITY_PRACTICES"), - ) - - -def test_add_high_risk_employee_when_given_notes_updates_notes( - runner, cli_state_with_user -): - notes = "being risky" - runner.invoke( - cli, - ["high-risk-employee", "add", TEST_EMPLOYEE, "--notes", notes], - obj=cli_state_with_user, - ) - cli_state_with_user.sdk.detectionlists.update_user_notes.assert_called_once_with( - TEST_ID, notes - ) - - -def test_add_high_risk_employee_when_user_does_not_exist_exits_with_correct_message( - runner, cli_state_without_user -): - result = runner.invoke( - cli, ["high-risk-employee", "add", TEST_EMPLOYEE], obj=cli_state_without_user - ) - assert result.exit_code == 1 - assert ( - f"User '{TEST_EMPLOYEE}' does not exist or you do not have permission to view them." - in result.output - ) - - -def test_add_high_risk_employee_when_user_already_added_exits_with_correct_message( - runner, cli_state_with_user, user_already_added_error -): - def add_user(user): - raise user_already_added_error - - cli_state_with_user.sdk.detectionlists.high_risk_employee.add.side_effect = add_user - - result = runner.invoke( - cli, ["high-risk-employee", "add", TEST_EMPLOYEE], obj=cli_state_with_user - ) - assert result.exit_code == 1 - assert "User with ID TEST_ID is already on the detection list" in result.output - - -def test_remove_high_risk_employee_calls_remove(runner, cli_state_with_user): - runner.invoke( - cli, ["high-risk-employee", "remove", TEST_EMPLOYEE], obj=cli_state_with_user - ) - cli_state_with_user.sdk.detectionlists.high_risk_employee.remove.assert_called_once_with( - TEST_ID - ) - - -def test_remove_high_risk_employee_when_user_does_not_exist_exits_with_correct_message( - runner, cli_state_without_user -): - result = runner.invoke( - cli, ["high-risk-employee", "remove", TEST_EMPLOYEE], obj=cli_state_without_user - ) - assert result.exit_code == 1 - assert ( - f"User '{TEST_EMPLOYEE}' does not exist or you do not have permission to view them." - in result.output - ) - - -def test_bulk_add_employees_calls_expected_py42_methods(runner, cli_state): - add_user_cloud_alias = thread_safe_side_effect() - add_user_risk_tags = thread_safe_side_effect() - update_user_notes = thread_safe_side_effect() - hre_add_user = thread_safe_side_effect() - - cli_state.sdk.detectionlists.add_user_cloud_alias.side_effect = add_user_cloud_alias - cli_state.sdk.detectionlists.add_user_risk_tags.side_effect = add_user_risk_tags - cli_state.sdk.detectionlists.update_user_notes.side_effect = update_user_notes - cli_state.sdk.detectionlists.high_risk_employee.add.side_effect = hre_add_user - - with runner.isolated_filesystem(): - with open("test_add.csv", "w") as csv: - csv.writelines( - [ - "username,cloud_alias,risk_tag,notes\n", - "test_user,test_alias,test_tag_1 test_tag_2,test_note\n", - "test_user_2,test_alias_2,test_tag_3,test_note_2\n", - "test_user_3,,,\n", - ] - ) - runner.invoke( - cli, ["high-risk-employee", "bulk", "add", "test_add.csv"], obj=cli_state - ) - alias_args = [call[1] for call in add_user_cloud_alias.call_args_list] - assert add_user_cloud_alias.call_count == 2 - assert "test_alias" in alias_args - assert "test_alias_2" in alias_args - - add_risk_tags_call_args = [call[1] for call in add_user_risk_tags.call_args_list] - assert add_user_risk_tags.call_count == 2 - assert ["test_tag_1", "test_tag_2"] in add_risk_tags_call_args - assert ["test_tag_3"] in add_risk_tags_call_args - - add_notes_call_args = [call[1] for call in update_user_notes.call_args_list] - assert update_user_notes.call_count == 2 - assert "test_note" in add_notes_call_args - assert "test_note_2" in add_notes_call_args - - assert hre_add_user.call_count == 3 - - -def test_bulk_remove_employees_uses_expected_arguments(runner, cli_state, mocker): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines(["username\n", "test@example.com\n", "test2@example.com"]) - runner.invoke( - cli, - ["high-risk-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com"}, - {"username": "test2@example.com"}, - ] - - -def test_bulk_remove_employees_uses_expected_arguments_when_extra_columns( - runner, cli_state, mocker -): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines( - [ - "username,test_column\n", - "test@example.com,test_value1\n", - "test2@example.com,test_value2\n", - ] - ) - runner.invoke( - cli, - ["high-risk-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com"}, - {"username": "test2@example.com"}, - ] - - -def test_bulk_remove_employees_uses_expected_arguments_when_flat_file( - runner, cli_state, mocker -): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.txt", "w") as csv: - csv.writelines(["# username\n", "test@example.com\n", "test2@example.com"]) - runner.invoke( - cli, - ["high-risk-employee", "bulk", "remove", "test_remove.txt"], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com"}, - {"username": "test2@example.com"}, - ] - - -def test_bulk_remove_employees_uses_expected_arguments_when_no_header( - runner, cli_state, mocker -): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove.csv", "w") as csv: - csv.writelines(["test@example.com\n", "test2@example.com"]) - runner.invoke( - cli, - ["high-risk-employee", "bulk", "remove", "test_remove.csv"], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com"}, - {"username": "test2@example.com"}, - ] - - -def test_bulk_add_risk_tags_uses_expected_arguments(runner, cli_state, mocker): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_add_risk_tags.csv", "w") as csv: - csv.writelines( - ["username,tag\n", "test@example.com,tag1\n", "test2@example.com,tag2"] - ) - runner.invoke( - cli, - ["high-risk-employee", "bulk", "add-risk-tags", "test_add_risk_tags.csv"], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com", "tag": "tag1"}, - {"username": "test2@example.com", "tag": "tag2"}, - ] - - -def test_bulk_remove_risk_tags_uses_expected_arguments(runner, cli_state, mocker): - bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process") - with runner.isolated_filesystem(): - with open("test_remove_risk_tags.csv", "w") as csv: - csv.writelines( - ["username,tag\n", "test@example.com,tag1\n", "test2@example.com,tag2"] - ) - runner.invoke( - cli, - [ - "high-risk-employee", - "bulk", - "remove-risk-tags", - "test_remove_risk_tags.csv", - ], - obj=cli_state, - ) - assert bulk_processor.call_args[0][1] == [ - {"username": "test@example.com", "tag": "tag1"}, - {"username": "test2@example.com", "tag": "tag2"}, - ] - - -def test_remove_high_risk_employee_when_user_not_on_list_prints_expected_error( - mocker, runner, cli_state -): - cli_state.sdk.detectionlists.high_risk_employee.remove.side_effect = ( - get_user_not_on_list_side_effect(mocker, "high-risk-employee") - ) - test_username = "test@example.com" - result = runner.invoke( - cli, ["high-risk-employee", "remove", test_username], obj=cli_state - ) - assert ( - f"User with ID '{TEST_ID}' is not currently on the high-risk-employee list." - in result.output - ) - - -@pytest.mark.parametrize( - "command, error_msg", - [ - (f"{HR_EMPLOYEE_COMMAND} add", "Missing argument 'USERNAME'."), - (f"{HR_EMPLOYEE_COMMAND} remove", "Missing argument 'USERNAME'."), - (f"{HR_EMPLOYEE_COMMAND} bulk add", "Missing argument 'CSV_FILE'."), - (f"{HR_EMPLOYEE_COMMAND} bulk remove", "Missing argument 'CSV_FILE'."), - (f"{HR_EMPLOYEE_COMMAND} bulk add-risk-tags", "Missing argument 'CSV_FILE'."), - ( - f"{HR_EMPLOYEE_COMMAND} bulk remove-risk-tags", - "Missing argument 'CSV_FILE'.", - ), - ], -) -def test_hr_employee_command_when_missing_required_parameters_returns_error( - command, error_msg, runner, cli_state -): - result = runner.invoke(cli, command.split(" "), obj=cli_state) - assert result.exit_code == 2 - assert error_msg in "".join(result.output) diff --git a/tests/integration/test_departing_employee.py b/tests/integration/test_departing_employee.py deleted file mode 100644 index f3dca5be..00000000 --- a/tests/integration/test_departing_employee.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest -from tests.integration.conftest import append_profile -from tests.integration.util import assert_test_is_successful - - -@pytest.mark.integration -def test_departing_employee_list_command_returns_success_return_code( - runner, integration_test_profile -): - command = "departing-employee list" - assert_test_is_successful(runner, append_profile(command)) diff --git a/tests/integration/test_high_risk_employee.py b/tests/integration/test_high_risk_employee.py deleted file mode 100644 index 26aeb550..00000000 --- a/tests/integration/test_high_risk_employee.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest -from tests.integration.conftest import append_profile -from tests.integration.util import assert_test_is_successful - - -@pytest.mark.integration -def test_high_risk_employee_list_command_returns_success_return_code( - runner, integration_test_profile -): - command = "high-risk-employee list" - assert_test_is_successful(runner, append_profile(command)) diff --git a/tests/logger/test_init.py b/tests/logger/test_init.py index b1948da9..640f67b9 100644 --- a/tests/logger/test_init.py +++ b/tests/logger/test_init.py @@ -77,23 +77,21 @@ def test_get_logger_for_server_when_given_cef_format_uses_cef_formatter(): logger = get_logger_for_server( "example.com", ServerProtocol.TCP, SendToFileEventsOutputFormat.CEF, None ) - assert type(logger.handlers[0].formatter) == FileEventDictToCEFFormatter + assert isinstance(logger.handlers[0].formatter, FileEventDictToCEFFormatter) def test_get_logger_for_server_when_given_json_format_uses_json_formatter(): logger = get_logger_for_server( "example.com", ServerProtocol.TCP, OutputFormat.JSON, None ) - actual = type(logger.handlers[0].formatter) - assert actual == FileEventDictToJSONFormatter + assert isinstance(logger.handlers[0].formatter, FileEventDictToJSONFormatter) def test_get_logger_for_server_when_given_raw_json_format_uses_raw_json_formatter(): logger = get_logger_for_server( "example.com", ServerProtocol.TCP, OutputFormat.RAW, None ) - actual = type(logger.handlers[0].formatter) - assert actual == FileEventDictToRawJSONFormatter + assert isinstance(logger.handlers[0].formatter, FileEventDictToRawJSONFormatter) def test_get_logger_for_server_when_called_twice_only_has_one_handler(): @@ -108,7 +106,7 @@ def test_get_logger_for_server_uses_no_priority_syslog_handler(): logger = get_logger_for_server( "example.com", ServerProtocol.TCP, SendToFileEventsOutputFormat.CEF, None ) - assert type(logger.handlers[0]) == NoPrioritySysLogHandler + assert isinstance(logger.handlers[0], NoPrioritySysLogHandler) def test_get_logger_for_server_constructs_handler_with_expected_args( diff --git a/tests/test_bulk.py b/tests/test_bulk.py index 4031de51..07c8badf 100644 --- a/tests/test_bulk.py +++ b/tests/test_bulk.py @@ -45,7 +45,8 @@ def test_generate_template_cmd_factory_returns_expected_command(): assert template.name == "generate-template" assert len(template.params) == 2 assert template.params[0].name == "cmd" - assert template.params[0].type.choices == ["add", "remove"] + assert "add" in template.params[0].type.choices + assert "remove" in template.params[0].type.choices assert template.params[1].name == "path" @@ -63,7 +64,8 @@ def test_generate_template_cmd_factory_when_using_defaults_returns_expected_comm assert template.name == "generate-template" assert len(template.params) == 2 assert template.params[0].name == "cmd" - assert template.params[0].type.choices == ["add", "remove"] + assert "add" in template.params[0].type.choices + assert "remove" in template.params[0].type.choices assert template.params[1].name == "path" diff --git a/tests/test_output_formats.py b/tests/test_output_formats.py index 4c98f95c..d8295dac 100644 --- a/tests/test_output_formats.py +++ b/tests/test_output_formats.py @@ -2,7 +2,7 @@ from collections import OrderedDict import pytest -from numpy import NaN +from numpy import nan as NaN from pandas import DataFrame import code42cli.output_formats as output_formats_module diff --git a/tox.ini b/tox.ini index 9413bdf0..b69f3de9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{311,310,39,38,37} + py{312,311,310,39} docs style skip_missing_interpreters = true @@ -12,6 +12,7 @@ deps = pytest-cov == 4.0.0 pandas >= 1.1.3 pexpect == 4.8.0 + setuptools >= 66.0.0 commands = # -v: verbose @@ -24,9 +25,9 @@ commands = [testenv:docs] deps = - sphinx == 4.4.0 - myst-parser == 0.17.2 - sphinx_rtd_theme == 1.0.0 + sphinx == 8.1.3 + myst-parser == 4.0.0 + sphinx_rtd_theme == 3.0.2 sphinx-click whitelist_externals = bash