bevor-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bevor_cli/__init__.py +0 -0
- bevor_cli/annotations/__init__.py +0 -0
- bevor_cli/annotations/confirmations.py +16 -0
- bevor_cli/annotations/query.py +25 -0
- bevor_cli/apps/agent.py +10 -0
- bevor_cli/apps/analyses.py +47 -0
- bevor_cli/apps/chat.py +16 -0
- bevor_cli/apps/codes.py +18 -0
- bevor_cli/apps/findings.py +22 -0
- bevor_cli/apps/main.py +61 -0
- bevor_cli/apps/nodes.py +17 -0
- bevor_cli/apps/projects.py +14 -0
- bevor_cli/apps/sessions.py +13 -0
- bevor_cli/apps/workspaces.py +16 -0
- bevor_cli/client.py +157 -0
- bevor_cli/commands/__init__.py +0 -0
- bevor_cli/commands/agent/__init__.py +0 -0
- bevor_cli/commands/agent/materialze_cmd.py +65 -0
- bevor_cli/commands/agent/setup_cmd.py +18 -0
- bevor_cli/commands/analyses/__init__.py +0 -0
- bevor_cli/commands/analyses/active.py +42 -0
- bevor_cli/commands/analyses/chats/__init__.py +0 -0
- bevor_cli/commands/analyses/chats/get.py +39 -0
- bevor_cli/commands/analyses/chats/list.py +64 -0
- bevor_cli/commands/analyses/chats/messages.py +41 -0
- bevor_cli/commands/analyses/chats/new.py +44 -0
- bevor_cli/commands/analyses/chats/resume.py +43 -0
- bevor_cli/commands/analyses/chats/session.py +295 -0
- bevor_cli/commands/analyses/child.py +81 -0
- bevor_cli/commands/analyses/commit.py +49 -0
- bevor_cli/commands/analyses/create.py +105 -0
- bevor_cli/commands/analyses/delete.py +65 -0
- bevor_cli/commands/analyses/diff.py +87 -0
- bevor_cli/commands/analyses/entrypoints.py +56 -0
- bevor_cli/commands/analyses/findings.py +129 -0
- bevor_cli/commands/analyses/fork.py +47 -0
- bevor_cli/commands/analyses/list.py +85 -0
- bevor_cli/commands/analyses/merge.py +46 -0
- bevor_cli/commands/analyses/retry.py +77 -0
- bevor_cli/commands/analyses/run.py +87 -0
- bevor_cli/commands/analyses/select.py +85 -0
- bevor_cli/commands/analyses/set_head.py +45 -0
- bevor_cli/commands/analyses/transitions.py +90 -0
- bevor_cli/commands/analyses/view.py +94 -0
- bevor_cli/commands/auth/__init__.py +7 -0
- bevor_cli/commands/auth/login.py +135 -0
- bevor_cli/commands/auth/logout.py +23 -0
- bevor_cli/commands/auth/whoami.py +21 -0
- bevor_cli/commands/codes/__init__.py +0 -0
- bevor_cli/commands/codes/list.py +90 -0
- bevor_cli/commands/codes/push.py +76 -0
- bevor_cli/commands/codes/select.py +93 -0
- bevor_cli/commands/codes/view.py +80 -0
- bevor_cli/commands/context/__init__.py +92 -0
- bevor_cli/commands/findings/__init__.py +0 -0
- bevor_cli/commands/findings/acknowledge.py +28 -0
- bevor_cli/commands/findings/add.py +236 -0
- bevor_cli/commands/findings/bulk.py +110 -0
- bevor_cli/commands/findings/delete.py +28 -0
- bevor_cli/commands/findings/edit.py +109 -0
- bevor_cli/commands/findings/get.py +62 -0
- bevor_cli/commands/findings/interactive.py +329 -0
- bevor_cli/commands/findings/list.py +108 -0
- bevor_cli/commands/findings/revert.py +28 -0
- bevor_cli/commands/graph/__init__.py +0 -0
- bevor_cli/commands/graph/call_chain.py +48 -0
- bevor_cli/commands/graph/edges.py +59 -0
- bevor_cli/commands/graph/interactive.py +207 -0
- bevor_cli/commands/graph/renderer.py +135 -0
- bevor_cli/commands/link/__init__.py +52 -0
- bevor_cli/commands/link/helpers.py +26 -0
- bevor_cli/commands/nodes/__init__.py +0 -0
- bevor_cli/commands/nodes/get.py +52 -0
- bevor_cli/commands/nodes/list.py +75 -0
- bevor_cli/commands/nodes/source.py +40 -0
- bevor_cli/commands/projects/__init__.py +0 -0
- bevor_cli/commands/projects/create.py +54 -0
- bevor_cli/commands/projects/list.py +57 -0
- bevor_cli/commands/projects/select.py +75 -0
- bevor_cli/commands/projects/view.py +49 -0
- bevor_cli/commands/sessions/__init__.py +0 -0
- bevor_cli/commands/sessions/list.py +36 -0
- bevor_cli/commands/sessions/revoke.py +35 -0
- bevor_cli/commands/workspaces/__init__.py +0 -0
- bevor_cli/commands/workspaces/list.py +50 -0
- bevor_cli/commands/workspaces/pick.py +20 -0
- bevor_cli/commands/workspaces/select.py +68 -0
- bevor_cli/commands/workspaces/view.py +48 -0
- bevor_cli/configuration/creds.py +52 -0
- bevor_cli/configuration/settings.py +54 -0
- bevor_cli/configuration/state.py +113 -0
- bevor_cli/findings_yaml.py +291 -0
- bevor_cli/main.py +33 -0
- bevor_cli/output.py +69 -0
- bevor_cli/select.py +64 -0
- bevor_cli/settings.py +4 -0
- bevor_cli-0.1.0.dist-info/METADATA +14 -0
- bevor_cli-0.1.0.dist-info/RECORD +100 -0
- bevor_cli-0.1.0.dist-info/WHEEL +4 -0
- bevor_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""``bevor code list`` — list code versions."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types.literals import CodeVersionStatus, Network
|
|
7
|
+
from bevor_sdk.types.requests.graph import CodeQueryParams
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from bevor_cli import output
|
|
12
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
13
|
+
from bevor_cli.annotations.query import ORDER, ORDER_BY, PAGE, PAGE_SIZE
|
|
14
|
+
from bevor_cli.client import BevorInternalClient
|
|
15
|
+
from bevor_cli.configuration.creds import Credentials
|
|
16
|
+
from bevor_cli.configuration.settings import Settings
|
|
17
|
+
from bevor_cli.configuration.state import State
|
|
18
|
+
from bevor_cli.output import console
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_code_versions(
|
|
22
|
+
page: PAGE = None,
|
|
23
|
+
page_size: PAGE_SIZE = None,
|
|
24
|
+
order: ORDER = None,
|
|
25
|
+
order_by: ORDER_BY = None,
|
|
26
|
+
project: Optional[str] = typer.Option(
|
|
27
|
+
None, "--project", help="Project to list for. Defaults to current configuration"
|
|
28
|
+
),
|
|
29
|
+
network: Optional[Network] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--network",
|
|
32
|
+
help="Network to filter by",
|
|
33
|
+
),
|
|
34
|
+
identifier: Optional[str] = typer.Option(
|
|
35
|
+
None, "--identifier", help="Identifier (commit, hash, address) to filter by"
|
|
36
|
+
),
|
|
37
|
+
status: Optional[CodeVersionStatus] = typer.Option(
|
|
38
|
+
None, "--status", help="Code status to filter by"
|
|
39
|
+
),
|
|
40
|
+
json_out: JSON_OUT = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""List code versions for the active project, or the passed --project."""
|
|
43
|
+
creds = Credentials.load()
|
|
44
|
+
creds.require_token()
|
|
45
|
+
|
|
46
|
+
state = State.load()
|
|
47
|
+
workspace_id = state.validate_workspace()
|
|
48
|
+
|
|
49
|
+
settings = Settings.load()
|
|
50
|
+
|
|
51
|
+
query_params = CodeQueryParams(
|
|
52
|
+
page=page,
|
|
53
|
+
page_size=page_size or settings.default_page_size,
|
|
54
|
+
order=order,
|
|
55
|
+
order_by=order_by,
|
|
56
|
+
network=network,
|
|
57
|
+
identifier=identifier,
|
|
58
|
+
status=status,
|
|
59
|
+
)
|
|
60
|
+
client = BevorInternalClient(
|
|
61
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project(priority=project)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
codes = client.codes.list(query_params)
|
|
65
|
+
|
|
66
|
+
if json_out:
|
|
67
|
+
console.print_json(codes.model_dump_json(indent=2))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not codes.results:
|
|
71
|
+
console.print("No code versions found.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
table = Table("ID", "STATUS", "CREATED", "METHOD")
|
|
75
|
+
|
|
76
|
+
for v in codes.results:
|
|
77
|
+
args: List[str | Text] = [
|
|
78
|
+
v.id,
|
|
79
|
+
v.status,
|
|
80
|
+
str(v.created_at.date()),
|
|
81
|
+
v.version_method,
|
|
82
|
+
]
|
|
83
|
+
if state.code_id == v.id:
|
|
84
|
+
args.append(Text("●", style="bold green"))
|
|
85
|
+
table.add_row(*args)
|
|
86
|
+
|
|
87
|
+
console.print(table)
|
|
88
|
+
|
|
89
|
+
if codes.more:
|
|
90
|
+
output.info(f"\nShowing {codes.page} of {codes.total_pages}. Increase with --page-size/-n.")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""``bevor code push`` — upload a directory as a code version."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.exceptions import NotFoundError
|
|
8
|
+
from bevor_sdk.helpers.source_fingerprint import zip_source_tree
|
|
9
|
+
from bevor_sdk.types import ID
|
|
10
|
+
|
|
11
|
+
from bevor_cli import output
|
|
12
|
+
from bevor_cli.annotations.confirmations import NO_WAIT, YES
|
|
13
|
+
from bevor_cli.client import BevorInternalClient
|
|
14
|
+
from bevor_cli.configuration.creds import Credentials
|
|
15
|
+
from bevor_cli.configuration.state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def push_code(
|
|
19
|
+
path: Optional[Path] = typer.Option(None, "--path", help="Source tree root (default: cwd)."),
|
|
20
|
+
no_wait: NO_WAIT = False,
|
|
21
|
+
yes: YES = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Upload your current working directory to produce a new code version. Automatically updates your configured code version."""
|
|
24
|
+
|
|
25
|
+
creds = Credentials.load()
|
|
26
|
+
creds.require_token()
|
|
27
|
+
|
|
28
|
+
state = State.load()
|
|
29
|
+
workspace_id = state.validate_workspace()
|
|
30
|
+
project_id = state.validate_project()
|
|
31
|
+
|
|
32
|
+
client = BevorInternalClient(creds=creds, workspace_id=workspace_id, project_id=project_id)
|
|
33
|
+
|
|
34
|
+
if not yes:
|
|
35
|
+
project = client.projects.get()
|
|
36
|
+
if not typer.confirm(f"Create code within project '{project.name}'?"):
|
|
37
|
+
raise typer.Exit(0)
|
|
38
|
+
|
|
39
|
+
root = path or Path.cwd()
|
|
40
|
+
try:
|
|
41
|
+
with output.spinner("Packaging source tree..."):
|
|
42
|
+
zip_bytes, n_files, content_hash = zip_source_tree(root)
|
|
43
|
+
except ValueError as exc:
|
|
44
|
+
output.error(str(exc), hint="Make sure you're in the project root directory.")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
size_kb = len(zip_bytes) // 1024
|
|
47
|
+
output.info(f"Compressed {n_files} files → {size_kb} KB")
|
|
48
|
+
|
|
49
|
+
if state.code_id and state.code_fingerprint == content_hash:
|
|
50
|
+
# requests are project-scoped, so a code that still resolves belongs to the linked project.
|
|
51
|
+
try:
|
|
52
|
+
client.codes.get(ID(state.code_id))
|
|
53
|
+
output.info("No changes detected from the current code version")
|
|
54
|
+
return
|
|
55
|
+
except NotFoundError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
with output.spinner("Uploading..."):
|
|
59
|
+
# no harm in this being called regardless.
|
|
60
|
+
signing_key = client.codes.generate_signing_key()
|
|
61
|
+
result = client.codes.upload_folder_zip(
|
|
62
|
+
signing_key=signing_key, zip_bytes=zip_bytes, with_streaming=True
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if result.data.status == "failed":
|
|
66
|
+
output.error("Code processing failed on the server.")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
state.set_code(result.data)
|
|
70
|
+
state.save()
|
|
71
|
+
|
|
72
|
+
if result.data.status in ("processing", "queued") and not no_wait:
|
|
73
|
+
with output.spinner("Processing code..."):
|
|
74
|
+
result.stream_completion()
|
|
75
|
+
|
|
76
|
+
output.success(f"Code version: {result.data.id}")
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""``bevor codes select`` — set the selected code version."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.exceptions import NotFoundError
|
|
8
|
+
from bevor_sdk.types import ID
|
|
9
|
+
from bevor_sdk.types.literals import CodeVersionStatus
|
|
10
|
+
from bevor_sdk.types.requests.graph import CodeQueryParams
|
|
11
|
+
from bevor_sdk.types.responses.graph import CodeMappingSchema
|
|
12
|
+
|
|
13
|
+
from bevor_cli import output
|
|
14
|
+
from bevor_cli.client import BevorInternalClient
|
|
15
|
+
from bevor_cli.configuration.creds import Credentials
|
|
16
|
+
from bevor_cli.configuration.settings import Settings
|
|
17
|
+
from bevor_cli.configuration.state import State
|
|
18
|
+
from bevor_cli.output import console
|
|
19
|
+
from bevor_cli.select import _CANCEL, _select
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def select_code(
|
|
23
|
+
code_id: Optional[str] = typer.Argument(
|
|
24
|
+
None, help="Code version id to select. Prompts with a list when omitted."
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Select a code version in the selected project by id, or pick one from a list."""
|
|
28
|
+
if code_id is None:
|
|
29
|
+
pick_code()
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
creds = Credentials.load()
|
|
33
|
+
creds.require_token()
|
|
34
|
+
|
|
35
|
+
state = State.load()
|
|
36
|
+
workspace_id = state.validate_workspace()
|
|
37
|
+
client = BevorInternalClient(
|
|
38
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
code = client.codes.get(ID(code_id))
|
|
43
|
+
except NotFoundError:
|
|
44
|
+
output.error("code not found in the configured project")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
state.set_code(code)
|
|
48
|
+
state.save()
|
|
49
|
+
output.info("Updated code configuration")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pick_code(
|
|
53
|
+
*,
|
|
54
|
+
page: Optional[int] = None,
|
|
55
|
+
status: Optional[CodeVersionStatus] = None,
|
|
56
|
+
page_size: Optional[int] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Interactive picker for code versions in the configured project."""
|
|
59
|
+
creds = Credentials.load()
|
|
60
|
+
creds.require_token()
|
|
61
|
+
|
|
62
|
+
state = State.load()
|
|
63
|
+
workspace_id = state.validate_workspace()
|
|
64
|
+
|
|
65
|
+
client = BevorInternalClient(
|
|
66
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
settings = Settings.load()
|
|
70
|
+
|
|
71
|
+
codes = client.codes.list(
|
|
72
|
+
CodeQueryParams(page=page, status=status, page_size=page_size or settings.default_page_size)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not codes.results:
|
|
76
|
+
console.print("No code versions found.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
choices = [
|
|
80
|
+
questionary.Choice(
|
|
81
|
+
title=f"{c.status} · {c.id} {c.created_at.date()}",
|
|
82
|
+
value=c,
|
|
83
|
+
)
|
|
84
|
+
for c in codes.results
|
|
85
|
+
] + [questionary.Choice(title="↵ Cancel", value=_CANCEL)]
|
|
86
|
+
|
|
87
|
+
code: CodeMappingSchema | None = _select("Select a code version:", choices)
|
|
88
|
+
if code is None:
|
|
89
|
+
raise typer.Exit(0)
|
|
90
|
+
|
|
91
|
+
state.set_code(code)
|
|
92
|
+
state.save()
|
|
93
|
+
output.info("Updated code configuration")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""``bevor codes view`` — show a code version (the selected one by default)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.helpers.source_fingerprint import zip_source_tree
|
|
8
|
+
from bevor_sdk.types import ID
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from bevor_cli import output
|
|
12
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
13
|
+
from bevor_cli.client import BevorInternalClient
|
|
14
|
+
from bevor_cli.configuration.creds import Credentials
|
|
15
|
+
from bevor_cli.configuration.state import State
|
|
16
|
+
from bevor_cli.output import console
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def view_code(
|
|
20
|
+
code_id: Optional[str] = typer.Argument(
|
|
21
|
+
None, help="Code version id. Defaults to the selected code version."
|
|
22
|
+
),
|
|
23
|
+
json_out: JSON_OUT = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Print details for a code version, or the selected one."""
|
|
26
|
+
creds = Credentials.load()
|
|
27
|
+
creds.require_token()
|
|
28
|
+
|
|
29
|
+
state = State.load()
|
|
30
|
+
workspace_id = state.validate_workspace()
|
|
31
|
+
|
|
32
|
+
code_id = code_id or state.code_id
|
|
33
|
+
if not code_id:
|
|
34
|
+
output.error(
|
|
35
|
+
"No code version is configured.",
|
|
36
|
+
hint="Run `bevor codes push` or `bevor codes select`.",
|
|
37
|
+
)
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
client = BevorInternalClient(
|
|
41
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
42
|
+
)
|
|
43
|
+
code = client.codes.get(ID(code_id))
|
|
44
|
+
|
|
45
|
+
if json_out:
|
|
46
|
+
console.print_json(code.model_dump_json(indent=2))
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
root = Path.cwd()
|
|
50
|
+
_, _, content_hash = zip_source_tree(root)
|
|
51
|
+
|
|
52
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
53
|
+
table.add_column(style="bold")
|
|
54
|
+
table.add_column()
|
|
55
|
+
|
|
56
|
+
table.add_row("ID", code.id)
|
|
57
|
+
table.add_row("Status", code.status)
|
|
58
|
+
table.add_row("Version method", code.version_method)
|
|
59
|
+
table.add_row("Fingerprint", code.version_fingerprint)
|
|
60
|
+
table.add_row("In sync (cwd)", str(content_hash == code.version_fingerprint))
|
|
61
|
+
table.add_row("Created", str(code.created_at.date()))
|
|
62
|
+
|
|
63
|
+
if code.network:
|
|
64
|
+
table.add_row("Network", code.network)
|
|
65
|
+
|
|
66
|
+
if code.branch:
|
|
67
|
+
table.add_row("Branch", code.branch)
|
|
68
|
+
|
|
69
|
+
if code.commit:
|
|
70
|
+
c = code.commit
|
|
71
|
+
sha_short = c.sha[:7] if len(c.sha) >= 7 else c.sha
|
|
72
|
+
msg = c.message.split("\n", 1)[0][:80]
|
|
73
|
+
table.add_row("Commit", f"{sha_short} {msg}")
|
|
74
|
+
|
|
75
|
+
if code.repository:
|
|
76
|
+
r = code.repository
|
|
77
|
+
table.add_row("GitHub", r.full_name)
|
|
78
|
+
table.add_row("Repo URL", r.url)
|
|
79
|
+
|
|
80
|
+
console.print(table)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""bevor context — show `.bevor` context for this directory."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.types import ID
|
|
8
|
+
|
|
9
|
+
from bevor_cli import output
|
|
10
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
11
|
+
from bevor_cli.client import BevorInternalBusinessClient, BevorInternalClient
|
|
12
|
+
from bevor_cli.configuration.creds import Credentials
|
|
13
|
+
from bevor_cli.configuration.state import State
|
|
14
|
+
from bevor_cli.output import console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_context(
|
|
18
|
+
json_out: JSON_OUT = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Show workspace, project, code, and analysis from `.bevor` (and live labels where available)."""
|
|
21
|
+
|
|
22
|
+
creds = Credentials.load()
|
|
23
|
+
creds.require_token()
|
|
24
|
+
|
|
25
|
+
state = State.load()
|
|
26
|
+
workspace_id = state.validate_workspace()
|
|
27
|
+
|
|
28
|
+
payload: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
workspace = BevorInternalBusinessClient(
|
|
32
|
+
creds=creds, workspace_id=workspace_id
|
|
33
|
+
).get_workspace()
|
|
34
|
+
except Exception:
|
|
35
|
+
output.error(
|
|
36
|
+
f"Workspace {state.workspace_id} no longer exists (deleted on dashboard?).",
|
|
37
|
+
hint="Run `bevor link` to reconfigure.",
|
|
38
|
+
)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
lines = [f"Workspace : {workspace.name} [{workspace.id}]"]
|
|
42
|
+
payload["workspace"] = workspace.model_dump(mode="json")
|
|
43
|
+
payload["project"] = None
|
|
44
|
+
payload["code"] = None
|
|
45
|
+
payload["analysis"] = None
|
|
46
|
+
|
|
47
|
+
if not state.project_id:
|
|
48
|
+
lines += ["Project : (not set)", "Code : (not set)", "Analysis : (not set)"]
|
|
49
|
+
else:
|
|
50
|
+
client = BevorInternalClient(
|
|
51
|
+
creds=creds, workspace_id=workspace_id, project_id=state.project_id
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
project = client.projects.get()
|
|
56
|
+
lines.append(f"Project : {project.name} [{project.id}]")
|
|
57
|
+
payload["project"] = project.model_dump(mode="json")
|
|
58
|
+
except Exception:
|
|
59
|
+
lines.append(f"Project : (stale — {state.project_id} not found)")
|
|
60
|
+
|
|
61
|
+
if state.code_id:
|
|
62
|
+
try:
|
|
63
|
+
code = client.codes.get(ID(state.code_id))
|
|
64
|
+
lines.append(f"Code : {code.id}")
|
|
65
|
+
payload["code"] = code.model_dump(mode="json")
|
|
66
|
+
except Exception:
|
|
67
|
+
lines.append(f"Code : (stale — {state.code_id} not found)")
|
|
68
|
+
else:
|
|
69
|
+
lines.append("Code : (not set)")
|
|
70
|
+
|
|
71
|
+
if state.analysis_id:
|
|
72
|
+
try:
|
|
73
|
+
analysis = client.analyses.get(ID(state.analysis_id))
|
|
74
|
+
lines.append(
|
|
75
|
+
f"Analysis : {analysis.name} {analysis.version.code_version.id} [{analysis.id}]"
|
|
76
|
+
)
|
|
77
|
+
payload["analysis"] = analysis.model_dump(mode="json")
|
|
78
|
+
except Exception:
|
|
79
|
+
lines.append(f"Analysis : (stale — {state.analysis_id} not found)")
|
|
80
|
+
else:
|
|
81
|
+
lines.append("Analysis : (not set)")
|
|
82
|
+
|
|
83
|
+
payload["config"] = state.model_dump(mode="json")
|
|
84
|
+
|
|
85
|
+
if json_out:
|
|
86
|
+
console.print_json(json.dumps(payload, indent=2, default=str))
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
for line in lines:
|
|
90
|
+
print(line)
|
|
91
|
+
|
|
92
|
+
output.info("\nChange any of these with `bevor \\[resource] select [id]`")
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""``bevor findings validate``."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from bevor_sdk.types import ID
|
|
5
|
+
|
|
6
|
+
from bevor_cli import output
|
|
7
|
+
from bevor_cli.client import BevorInternalClient
|
|
8
|
+
from bevor_cli.configuration.creds import Credentials
|
|
9
|
+
from bevor_cli.configuration.state import State
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def acknowledge_finding(
|
|
13
|
+
finding_id: str = typer.Argument(..., help="Finding id to toggle validation for."),
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Toggle acknowledgement status for a finding."""
|
|
16
|
+
creds = Credentials.load()
|
|
17
|
+
creds.require_token()
|
|
18
|
+
|
|
19
|
+
state = State.load()
|
|
20
|
+
workspace_id = state.validate_workspace()
|
|
21
|
+
|
|
22
|
+
client = BevorInternalClient(
|
|
23
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
client.findings.toggle_acknowledgement(ID(finding_id))
|
|
27
|
+
|
|
28
|
+
output.success("Finding validation toggled.")
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, cast, get_args
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from bevor_sdk.types import ID
|
|
6
|
+
from bevor_sdk.types.literals import FindingLevel, FindingType
|
|
7
|
+
from bevor_sdk.types.requests.security import (
|
|
8
|
+
FindingBodyDict,
|
|
9
|
+
FindingLocationLogicalDict,
|
|
10
|
+
FindingLocationNodeIdDict,
|
|
11
|
+
FindingLocationPhysicalDict,
|
|
12
|
+
FindingMetadataDict,
|
|
13
|
+
)
|
|
14
|
+
from bevor_sdk.types.requests.security.findings import AddFindingBodyDict
|
|
15
|
+
from rich.prompt import Confirm, Prompt
|
|
16
|
+
|
|
17
|
+
from bevor_cli import output
|
|
18
|
+
from bevor_cli.client import BevorInternalClient
|
|
19
|
+
from bevor_cli.configuration.creds import Credentials
|
|
20
|
+
from bevor_cli.configuration.state import State
|
|
21
|
+
from bevor_cli.findings_yaml import (
|
|
22
|
+
FindingFormatError,
|
|
23
|
+
FindingYamlParser,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _pick_from_list(options: list[str], title: str) -> str:
|
|
28
|
+
typer.echo(title)
|
|
29
|
+
for i, x in enumerate(options, 1):
|
|
30
|
+
typer.echo(f" [{i}] {x}")
|
|
31
|
+
raw = typer.prompt(f"Number 1–{len(options)}, or type the value", default="1").strip()
|
|
32
|
+
try:
|
|
33
|
+
idx = int(raw)
|
|
34
|
+
if 1 <= idx <= len(options):
|
|
35
|
+
return options[idx - 1]
|
|
36
|
+
except ValueError:
|
|
37
|
+
pass
|
|
38
|
+
if raw in options:
|
|
39
|
+
return raw
|
|
40
|
+
output.error(f"Unknown choice: {raw!r}")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _interactive_parsed_doc(
|
|
45
|
+
tool: str | None = None, node_id: Optional[str] = None
|
|
46
|
+
) -> FindingBodyDict:
|
|
47
|
+
if node_id:
|
|
48
|
+
scope_raw = node_id
|
|
49
|
+
typer.echo(f"Entry point node id: {scope_raw}")
|
|
50
|
+
else:
|
|
51
|
+
scope_raw = ""
|
|
52
|
+
while not scope_raw:
|
|
53
|
+
scope_raw = Prompt.ask("Entry point node id", show_default=False).strip()
|
|
54
|
+
if not scope_raw:
|
|
55
|
+
output.info("Scope is required.")
|
|
56
|
+
|
|
57
|
+
location = FindingLocationNodeIdDict(strategy="node_id", node_id=scope_raw)
|
|
58
|
+
|
|
59
|
+
finding_body = FindingBodyDict(tool=tool, location=location)
|
|
60
|
+
|
|
61
|
+
has_finding = Confirm.ask(
|
|
62
|
+
"Is there a finding associated with this node? ('n' if you just want to mark the node as analyzed)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if has_finding:
|
|
66
|
+
ftype = Prompt.ask("Finding type:")
|
|
67
|
+
lvl = _pick_from_list(list(get_args(FindingLevel)), "Severity level:")
|
|
68
|
+
name = ""
|
|
69
|
+
while not name:
|
|
70
|
+
name = Prompt.ask("Finding title / name")
|
|
71
|
+
if not name:
|
|
72
|
+
output.info("Name is required.")
|
|
73
|
+
|
|
74
|
+
explanation = ""
|
|
75
|
+
while not explanation:
|
|
76
|
+
explanation = Prompt.ask("Explanation").strip()
|
|
77
|
+
if not explanation:
|
|
78
|
+
output.info("Explanation is required.")
|
|
79
|
+
|
|
80
|
+
rec = Prompt.ask("Recommendation (optional)", default="", show_default=False).strip()
|
|
81
|
+
poc = Prompt.ask("POC (optional)", default="", show_default=False).strip()
|
|
82
|
+
|
|
83
|
+
finding_body["metadata"] = FindingMetadataDict(
|
|
84
|
+
type=cast(FindingType, ftype),
|
|
85
|
+
level=cast(FindingLevel, lvl),
|
|
86
|
+
name=name.strip(),
|
|
87
|
+
explanation=explanation,
|
|
88
|
+
recommendation=rec,
|
|
89
|
+
poc=poc,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return finding_body
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def add_finding(
|
|
96
|
+
version: Optional[str] = typer.Option(
|
|
97
|
+
None,
|
|
98
|
+
"--version",
|
|
99
|
+
help="Version id to stage against (default: configured analysis head).",
|
|
100
|
+
),
|
|
101
|
+
file: Path | None = typer.Option(
|
|
102
|
+
None, "--file", help="YAML file containing exactly one finding mapping."
|
|
103
|
+
),
|
|
104
|
+
interactive: bool = typer.Option(False, "--interactive", help="Force interactive prompts."),
|
|
105
|
+
# finding fields
|
|
106
|
+
type_: Optional[str] = typer.Option(None, "--type"),
|
|
107
|
+
level: Optional[FindingLevel] = typer.Option(None, "--level"),
|
|
108
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
109
|
+
explanation: Optional[str] = typer.Option(None, "--explanation"),
|
|
110
|
+
recommendation: Optional[str] = typer.Option(None, "--recommendation"),
|
|
111
|
+
poc: Optional[str] = typer.Option(None, "--poc"),
|
|
112
|
+
tool: Optional[str] = typer.Option(None, "--tool"),
|
|
113
|
+
# node_id
|
|
114
|
+
node_id: Optional[str] = typer.Option(None, "--node"),
|
|
115
|
+
location_id: Optional[str] = typer.Option(None, "--location"),
|
|
116
|
+
# physical
|
|
117
|
+
uri: Optional[str] = typer.Option(None, "--uri"),
|
|
118
|
+
start_line: Optional[int] = typer.Option(None, "--start-line"),
|
|
119
|
+
start_column: Optional[int] = typer.Option(None, "--start-column"),
|
|
120
|
+
end_line: Optional[int] = typer.Option(None, "--end-line"),
|
|
121
|
+
end_column: Optional[int] = typer.Option(None, "--end-column"),
|
|
122
|
+
char_offset: Optional[int] = typer.Option(None, "--char-offset"),
|
|
123
|
+
char_length: Optional[int] = typer.Option(None, "--char-length"),
|
|
124
|
+
byte_offset: Optional[int] = typer.Option(None, "--byte-offset"),
|
|
125
|
+
byte_length: Optional[int] = typer.Option(None, "--byte-length"),
|
|
126
|
+
# logical
|
|
127
|
+
fully_qualified_name: Optional[str] = typer.Option(None, "--fqn"),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Stage a new finding to your currently configured analysis, or to the --version.
|
|
130
|
+
|
|
131
|
+
Provide input one of three ways:
|
|
132
|
+
|
|
133
|
+
- ``--file``: a YAML file with exactly one finding mapping.
|
|
134
|
+
- ``--interactive``: prompts for each field.
|
|
135
|
+
- Flags directly, as described below.
|
|
136
|
+
|
|
137
|
+
When passing flags, specify a location, which specifies the code node that the finding is associated with, using exactly one of:
|
|
138
|
+
|
|
139
|
+
- ``--node`` and/or ``--location`` (a Bevor node id; ``--location`` alone
|
|
140
|
+
fans the finding out to every entrypoint that reaches it)
|
|
141
|
+
- ``--uri`` plus one full pair of ``--start-line``/``--end-line``,
|
|
142
|
+
``--char-offset``/``--char-length``, or ``--byte-offset``/``--byte-length``
|
|
143
|
+
- ``--fqn``
|
|
144
|
+
|
|
145
|
+
Metadata (``--type``, ``--level``, ``--name``, ``--explanation``) is all or
|
|
146
|
+
nothing: provide all four to record a finding, or omit all four to mark the
|
|
147
|
+
location as analyzed with no finding. ``--recommendation``, ``--reference``,
|
|
148
|
+
and ``--tool`` are always optional.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
creds = Credentials.load()
|
|
152
|
+
creds.require_token()
|
|
153
|
+
|
|
154
|
+
state = State.load()
|
|
155
|
+
workspace_id = state.validate_workspace()
|
|
156
|
+
client = BevorInternalClient(
|
|
157
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
body = AddFindingBodyDict(findings=[])
|
|
161
|
+
if version is None:
|
|
162
|
+
analysis_id = state.validate_analysis()
|
|
163
|
+
else:
|
|
164
|
+
version_id = ID(version)
|
|
165
|
+
v_ = client.analysis_versions.get(version_id)
|
|
166
|
+
analysis_id = v_.analysis_meta.id
|
|
167
|
+
body["version_id"] = version_id
|
|
168
|
+
|
|
169
|
+
finding_body = None
|
|
170
|
+
location = None
|
|
171
|
+
if interactive:
|
|
172
|
+
finding_body = _interactive_parsed_doc(node_id=node_id, tool=tool)
|
|
173
|
+
elif file is not None:
|
|
174
|
+
try:
|
|
175
|
+
finding_yaml = FindingYamlParser.from_file(path=file)
|
|
176
|
+
finding_body = finding_yaml.parse()
|
|
177
|
+
except OSError as exc:
|
|
178
|
+
output.error(f"Cannot read file: {exc}")
|
|
179
|
+
raise typer.Exit(1) from exc
|
|
180
|
+
except FindingFormatError as exc:
|
|
181
|
+
output.error(
|
|
182
|
+
str(exc),
|
|
183
|
+
hint=(
|
|
184
|
+
"One YAML mapping per file. For many findings use "
|
|
185
|
+
"``bevor agent submit`` "
|
|
186
|
+
"(one ``.yaml`` per finding under ``--dir``)."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(1) from exc
|
|
190
|
+
elif node_id:
|
|
191
|
+
location = FindingLocationNodeIdDict(
|
|
192
|
+
strategy="node_id", node_id=node_id, location_id=location_id
|
|
193
|
+
)
|
|
194
|
+
elif uri:
|
|
195
|
+
location = FindingLocationPhysicalDict(
|
|
196
|
+
strategy="physical",
|
|
197
|
+
uri=uri,
|
|
198
|
+
startLine=start_line,
|
|
199
|
+
startColumn=start_column,
|
|
200
|
+
endLine=end_line,
|
|
201
|
+
endColumn=end_column,
|
|
202
|
+
charOffset=char_offset,
|
|
203
|
+
charLength=char_length,
|
|
204
|
+
byteOffset=byte_offset,
|
|
205
|
+
byteLength=byte_length,
|
|
206
|
+
)
|
|
207
|
+
elif fully_qualified_name:
|
|
208
|
+
location = FindingLocationLogicalDict(
|
|
209
|
+
strategy="logical", fully_qualified_name=fully_qualified_name
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
output.error(
|
|
213
|
+
"--interactive mode, a file path, or location by node, physical, or logical is required"
|
|
214
|
+
)
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
|
|
217
|
+
if not finding_body:
|
|
218
|
+
assert location is not None
|
|
219
|
+
|
|
220
|
+
finding_body = FindingBodyDict(tool=tool, location=location)
|
|
221
|
+
|
|
222
|
+
if type_ and level and name and explanation:
|
|
223
|
+
finding_body["metadata"] = FindingMetadataDict(
|
|
224
|
+
type=type_,
|
|
225
|
+
level=level,
|
|
226
|
+
name=name,
|
|
227
|
+
explanation=explanation,
|
|
228
|
+
recommendation=recommendation,
|
|
229
|
+
poc=poc,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
body["findings"] = [finding_body]
|
|
233
|
+
|
|
234
|
+
findings_added = client.analyses.add_findings(analysis_id=analysis_id, body=body)
|
|
235
|
+
|
|
236
|
+
output.success(f"{len(findings_added.results)} Finding staged.")
|