Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ conviso --help

## Usage (examples)
- Projects: `python -m conviso.app projects list --company-id 443 --all`
- Projects (assignee filter): `python -m conviso.app projects list --company-id 443 --filter assignee=analyst@company.com --all`
- Project requirements + activities: `python -m conviso.app projects requirements --project-id 12345`
- Assets: `python -m conviso.app assets list --company-id 443 --tags cloud --attack-surface INTERNET_FACING --all`
- Requirements: `python -m conviso.app requirements create --company-id 443 --label "Req" --description "Desc" --activity "Login|Check login|REF-123"`
Expand All @@ -100,6 +101,7 @@ conviso --help
Output options: `--format table|json|csv`, `--output path` to save JSON/CSV.

Notes:
- `projects list --filter` supports `assignee=<email-or-name>` to filter by allocated analyst.
- GraphQL errors return exit code 1.
- Use `--all` on list commands to fetch every page.
- `--quiet` silences info logs; `--verbose` shows per-page requests when paginating.
Expand Down
2 changes: 1 addition & 1 deletion src/conviso/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.0
0.3.1
38 changes: 34 additions & 4 deletions src/conviso/commands/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def list_projects(
None,
"--filter",
"-F",
help="Apply filters in 'field=value' format. Supports aliases (e.g., id=123, name=foo, status=DONE).",
help="Apply filters in 'field=value' format. Supports aliases (e.g., id=123, name=foo, status=DONE, assignee=user@company.com).",
),
sort_by: Optional[str] = typer.Option(
None,
Expand All @@ -46,14 +46,20 @@ def list_projects(

# Build search parameters
params = {"scopeIdEq": company_id}
assignee_filter = None
if filters:
for f in filters:
if "=" not in f:
warning(f"[WARN] Invalid filter syntax: {f} (expected key=value)")
continue
key, value = f.split("=", 1)
gql_key = schema.resolve_filter_key(key.strip())
casted = schema.cast_filter_value(gql_key, value.strip())
key = key.strip()
value = value.strip()
if key.lower() == "assignee":
assignee_filter = value.lower()
continue
gql_key = schema.resolve_filter_key(key)
casted = schema.cast_filter_value(gql_key, value)
params[gql_key] = casted

variables = {
Expand Down Expand Up @@ -90,6 +96,9 @@ def list_projects(
environmentCompromised
projectType { label }
tags { name }
allocatedAnalyst {
portalUser { name email }
}
assets { id name }
requirementsProgress { done total }
}
Expand All @@ -99,6 +108,7 @@ def list_projects(
"""

try:
fetch_all = all_pages or bool(assignee_filter)
current_page = page
rows = []
total_pages = None
Expand All @@ -119,6 +129,21 @@ def list_projects(
break

for p in collection:
assignees = []
for alloc in p.get("allocatedAnalyst") or []:
portal_user = (alloc or {}).get("portalUser") or {}
email = (portal_user.get("email") or "").strip()
name = (portal_user.get("name") or "").strip()
if email:
assignees.append(email)
elif name:
assignees.append(name)

if assignee_filter:
haystack = " ".join(assignees).lower()
if assignee_filter not in haystack:
continue

done = (p.get("requirementsProgress") or {}).get("done", 0)
total = (p.get("requirementsProgress") or {}).get("total", 0)
requirements = f"{done}/{total}"
Expand Down Expand Up @@ -148,10 +173,13 @@ def list_projects(
"tags": tags_str,
})

if not all_pages or (total_pages is not None and current_page >= total_pages):
if not fetch_all or (total_pages is not None and current_page >= total_pages):
break
current_page += 1

if not rows:
typer.echo("⚠️ No projects found.")
raise typer.Exit()

export_data(
rows,
Expand All @@ -175,6 +203,8 @@ def list_projects(
f"(page {page}/{total_pages_calc}).\n"
)

except typer.Exit:
raise
except Exception as e:
error(f"Error listing projects: {e}")
raise typer.Exit(code=1)
Expand Down