diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d494cd0..14010ee 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -7,6 +7,7 @@ on:
- main
paths:
- 'cortexapps_cli/**'
+ - 'docker/**'
- 'pyproject.toml'
- 'poetry.lock'
diff --git a/HISTORY.md b/HISTORY.md
index 9f889ec..be45d37 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [1.19.2](https://github.com/cortexapps/cli/releases/tag/1.19.2) - 2026-06-10
+
+[Compare with 1.19.1](https://github.com/cortexapps/cli/compare/1.19.1...1.19.2)
+
+### Bug Fixes
+
+- update Docker base image for CVE-2026-45447 and add docker/ to publish triggers #patch ([1bcef87](https://github.com/cortexapps/cli/commit/1bcef8718845cad9b15094a764392c5dcaa8804e) by Jeff Schnitter).
+
+## [1.19.1](https://github.com/cortexapps/cli/releases/tag/1.19.1) - 2026-06-09
+
+[Compare with 1.19.0](https://github.com/cortexapps/cli/compare/1.19.0...1.19.1)
+
+### Bug Fixes
+
+- remove invalid RST transition that breaks PyPI rendering #patch ([00e2d96](https://github.com/cortexapps/cli/commit/00e2d96500ddfb9325d8db8075612af4a0489d58) by Jeff Schnitter).
+
## [1.19.0](https://github.com/cortexapps/cli/releases/tag/1.19.0) - 2026-06-01
[Compare with 1.18.0](https://github.com/cortexapps/cli/compare/1.18.0...1.19.0)
diff --git a/README.rst b/README.rst
index 3f1f4d2..dc9c1ca 100644
--- a/README.rst
+++ b/README.rst
@@ -453,8 +453,6 @@ This recipe creates YAML files for each Workflow. This may be helpful if you ar
cortex workflows get --tag $workflow --yaml > $workflow.yaml
done
-====================================
-
.. |PyPI download month| image:: https://img.shields.io/pypi/dm/cortexapps-cli.svg
:target: https://pypi.python.org/pypi/cortexapps-cli/
.. |PyPI version shields.io| image:: https://img.shields.io/pypi/v/cortexapps-cli.svg
diff --git a/cortexapps_cli/cli.py b/cortexapps_cli/cli.py
index 1327aab..5664c5c 100755
--- a/cortexapps_cli/cli.py
+++ b/cortexapps_cli/cli.py
@@ -40,6 +40,7 @@
import cortexapps_cli.commands.scorecards as scorecards
import cortexapps_cli.commands.secrets as secrets
import cortexapps_cli.commands.teams as teams
+import cortexapps_cli.commands.users as users
import cortexapps_cli.commands.workflows as workflows
app = typer.Typer(
@@ -77,6 +78,7 @@
app.add_typer(scorecards.app, name="scorecards")
app.add_typer(secrets.app, name="secrets")
app.add_typer(teams.app, name="teams")
+app.add_typer(users.app, name="users")
app.add_typer(workflows.app, name="workflows")
# global options
diff --git a/cortexapps_cli/commands/users.py b/cortexapps_cli/commands/users.py
new file mode 100644
index 0000000..cf9b26e
--- /dev/null
+++ b/cortexapps_cli/commands/users.py
@@ -0,0 +1,5 @@
+import typer
+import cortexapps_cli.commands.users_commands.roles as roles
+
+app = typer.Typer(help="Users commands", no_args_is_help=True)
+app.add_typer(roles.app, name="roles")
diff --git a/cortexapps_cli/commands/users_commands/roles.py b/cortexapps_cli/commands/users_commands/roles.py
new file mode 100644
index 0000000..440264f
--- /dev/null
+++ b/cortexapps_cli/commands/users_commands/roles.py
@@ -0,0 +1,49 @@
+from typing import List, Optional
+import typer
+from cortexapps_cli.command_options import ListCommandOptions
+from cortexapps_cli.utils import print_output_with_context
+
+app = typer.Typer(help="Roles commands", no_args_is_help=True)
+
+@app.command()
+def list(
+ ctx: typer.Context,
+ email: Optional[List[str]] = typer.Option(None, "--email", "-e", help="Filter by email address; can be specified multiple times", show_default=False),
+ page: ListCommandOptions.page = None,
+ page_size: ListCommandOptions.page_size = 250,
+ table_output: ListCommandOptions.table_output = False,
+ csv_output: ListCommandOptions.csv_output = False,
+ columns: ListCommandOptions.columns = [],
+ no_headers: ListCommandOptions.no_headers = False,
+ filters: ListCommandOptions.filters = [],
+ sort: ListCommandOptions.sort = [],
+):
+ """
+ List user role assignments. The API key used to make the request must have the View Roles permission.
+ """
+
+ client = ctx.obj["client"]
+
+ params = {
+ "page": page,
+ "pageSize": page_size,
+ }
+
+ if email:
+ params["email"] = ",".join(email)
+
+ if (table_output or csv_output) and not ctx.params.get('columns'):
+ ctx.params['columns'] = [
+ "Email=email",
+ "Name=name",
+ "Roles=roles",
+ ]
+
+ # remove any params that are None
+ params = {k: v for k, v in params.items() if v is not None}
+
+ if page is None:
+ r = client.fetch("api/v1/users/roles", params=params)
+ else:
+ r = client.get("api/v1/users/roles", params=params)
+ print_output_with_context(ctx, r)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8f71e1a..3a55b9d 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,10 +1,7 @@
FROM python:3.13-slim
RUN pip install --upgrade pip
-RUN apt update && apt install -y jq yq
-# Installing this version to address security vulnerability reported in CVE-2024-33599
-# This line should come out once the fix is included in a python image.
-RUN apt install -y libc-bin
+RUN apt update && apt upgrade -y && apt install -y jq yq
RUN useradd -m cortex
ADD config /home/cortex/.cortex/config
diff --git a/docs/superpowers/specs/2026-06-10-users-roles-command-design.md b/docs/superpowers/specs/2026-06-10-users-roles-command-design.md
new file mode 100644
index 0000000..b3cb948
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-10-users-roles-command-design.md
@@ -0,0 +1,98 @@
+# Users Roles Command Design
+
+## Summary
+
+Add a `cortex users roles list` command to support the `GET /api/v1/users/roles` endpoint. The command is structured as a subcommand group (`users` > `roles` > `list`) to accommodate future user and role management endpoints.
+
+## API Endpoint
+
+**`GET /api/v1/users/roles`**
+
+Query parameters:
+- `email` (optional): Comma-separated email addresses, case-insensitive
+- `pageSize` (optional): 1-1000, defaults to 250
+- `page` (optional): Zero-indexed, defaults to 0
+
+Response:
+```json
+{
+ "page": 0,
+ "total": 100,
+ "totalPages": 1,
+ "users": [
+ {
+ "email": "user@example.com",
+ "name": "User Name",
+ "roles": [
+ { "type": "BASIC", "role": "ADMIN" },
+ { "type": "CUSTOM", "name": "My Role", "tag": "my-role" }
+ ]
+ }
+ ]
+}
+```
+
+Requires Bearer token with "View Roles" permission.
+
+## File Structure
+
+```
+cortexapps_cli/commands/
+ users.py # Top-level users Typer app, imports roles subcommand
+ users_commands/
+ __init__.py
+ roles.py # roles subcommand with `list` command
+```
+
+Registration in `cli.py`:
+```python
+import cortexapps_cli.commands.users as users
+app.add_typer(users.app, name="users")
+```
+
+Follows the existing `scorecards` / `scorecards_commands/exemptions` pattern.
+
+## Command: `cortex users roles list`
+
+### Options
+
+Standard `ListCommandOptions`:
+- `--page, -p`: Page number (omit to fetch all pages)
+- `--page-size, -z`: Results per page (default 250)
+- `--table`: Table output
+- `--csv`: CSV output
+- `--columns, -C`: Column selection
+- `--no-headers`: Suppress table/CSV headers
+- `--filter, -F`: Row filtering
+- `--sort, -S`: Row sorting
+
+Custom:
+- `--email, -e`: Repeatable option to filter by email address(es). Multiple values joined comma-separated for the API.
+
+### Default Table Columns
+
+When `--table` or `--csv` is used without explicit `--columns`:
+- `Email=email`
+- `Name=name`
+- `Roles=roles`
+
+### Behavior
+
+- No `--page`: calls `client.fetch()` for all pages
+- With `--page`: calls `client.get()` for a single page
+- Output via `print_output_with_context()`
+
+## Future Extensibility
+
+- `users.py` will host top-level user commands (e.g., `users get`, `users create`)
+- `users_commands/roles.py` will host additional role commands (e.g., `roles create`, `roles update`)
+
+## Testing
+
+Integration tests against jeff-sandbox tenant with Okta SCIM provisioned users. Test file: `tests/test_users.py`.
+
+Tests:
+- `test_users_roles_list`: List all user role assignments
+- `test_users_roles_list_with_email_filter`: Filter by one or more emails
+- `test_users_roles_list_table_output`: Verify table formatting
+- `test_users_roles_list_pagination`: Single page retrieval
diff --git a/tests/test_users.py b/tests/test_users.py
new file mode 100644
index 0000000..7d18f0a
--- /dev/null
+++ b/tests/test_users.py
@@ -0,0 +1,50 @@
+import pytest
+from tests.helpers.utils import *
+
+def test_users_roles_list():
+ response = cli(["users", "roles", "list"])
+ assert "users" in response, "Response should contain 'users' key"
+ assert len(response["users"]) > 0, "Should have at least one user with role assignments"
+
+ first_user = response["users"][0]
+ assert "email" in first_user, "User should have an email field"
+ assert "name" in first_user, "User should have a name field"
+ assert "roles" in first_user, "User should have a roles field"
+
+def test_users_roles_list_with_email_filter():
+ # First, get a valid email from the full list
+ response = cli(["users", "roles", "list"])
+ email = response["users"][0]["email"]
+
+ # Filter by that email
+ response = cli(["users", "roles", "list", "-e", email])
+ assert len(response["users"]) == 1, "Should return exactly one user"
+ assert response["users"][0]["email"].lower() == email.lower(), "Returned user email should match filter"
+
+def test_users_roles_list_with_multiple_email_filter():
+ # Get two emails from the full list
+ response = cli(["users", "roles", "list"])
+ if len(response["users"]) < 2:
+ pytest.skip("Need at least 2 users to test multiple email filter")
+
+ email1 = response["users"][0]["email"]
+ email2 = response["users"][1]["email"]
+
+ response = cli(["users", "roles", "list", "-e", email1, "-e", email2])
+ assert len(response["users"]) == 2, "Should return exactly two users"
+ returned_emails = {u["email"].lower() for u in response["users"]}
+ assert email1.lower() in returned_emails, "First filtered email should be in results"
+ assert email2.lower() in returned_emails, "Second filtered email should be in results"
+
+def test_users_roles_list_table_output():
+ response = cli(["users", "roles", "list", "--table"], ReturnType.STDOUT)
+ assert "Email" in response, "Table output should contain Email header"
+ assert "Name" in response, "Table output should contain Name header"
+ assert "Roles" in response, "Table output should contain Roles header"
+
+def test_users_roles_list_pagination():
+ response = cli(["users", "roles", "list", "-p", "0", "-z", "1"])
+ assert "users" in response, "Response should contain 'users' key"
+ assert len(response["users"]) <= 1, "Should return at most 1 user per page"
+ assert "page" in response, "Response should contain 'page' field"
+ assert "total" in response, "Response should contain 'total' field"