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,77 @@
|
|
|
1
|
+
"""``bevor analysis retry`` — retry a failed pipeline."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk._sync.resources._modules import EventResource
|
|
7
|
+
from bevor_sdk.types import ID
|
|
8
|
+
from bevor_sdk.types.requests.security import VersionBodyDict
|
|
9
|
+
|
|
10
|
+
from bevor_cli import output
|
|
11
|
+
from bevor_cli.annotations.confirmations import NO_WAIT, SILENT
|
|
12
|
+
from bevor_cli.client import BevorInternalClient
|
|
13
|
+
from bevor_cli.commands.analyses.run import render_status
|
|
14
|
+
from bevor_cli.configuration.creds import Credentials
|
|
15
|
+
from bevor_cli.configuration.state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def retry_analysis(
|
|
19
|
+
version: Optional[str] = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--version",
|
|
22
|
+
help="Version id to retry (default: configured analysis head).",
|
|
23
|
+
),
|
|
24
|
+
no_wait: NO_WAIT = False,
|
|
25
|
+
silent: SILENT = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Retry failed segments of a Bevor Security pipeline run for the configured analysis head (or ``--version``). See `bevor analyses run`."""
|
|
28
|
+
creds = Credentials.load()
|
|
29
|
+
creds.require_token()
|
|
30
|
+
|
|
31
|
+
state = State.load()
|
|
32
|
+
workspace_id = state.validate_workspace()
|
|
33
|
+
|
|
34
|
+
client = BevorInternalClient(
|
|
35
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
wait = not no_wait
|
|
39
|
+
|
|
40
|
+
opts: VersionBodyDict = {}
|
|
41
|
+
if version:
|
|
42
|
+
v_ = client.analysis_versions.get(ID(version))
|
|
43
|
+
analysis_id = v_.analysis_meta.id
|
|
44
|
+
version_id = v_.id
|
|
45
|
+
opts["version_id"] = version_id
|
|
46
|
+
else:
|
|
47
|
+
analysis_id = state.validate_analysis()
|
|
48
|
+
|
|
49
|
+
run_result = client.analyses.retry(analysis_id, opts, with_streaming=wait)
|
|
50
|
+
|
|
51
|
+
if wait and isinstance(run_result, EventResource):
|
|
52
|
+
ep_map = {}
|
|
53
|
+
eps = client.analyses.entrypoints(analysis_id, opts)
|
|
54
|
+
for ep in eps.results:
|
|
55
|
+
ep_map[ep.node.id] = (ep.node.short_id, ep.node.name, ep.pipeline_status)
|
|
56
|
+
|
|
57
|
+
if not silent:
|
|
58
|
+
with output.spinner("Analysis running…"):
|
|
59
|
+
for event in run_result.stream_completion():
|
|
60
|
+
if event.id in ep_map:
|
|
61
|
+
short_id, name, _ = ep_map[event.id]
|
|
62
|
+
ep_map[event.id] = (short_id, name, event.status)
|
|
63
|
+
render_status(ep_map)
|
|
64
|
+
else:
|
|
65
|
+
for event in run_result.stream_completion():
|
|
66
|
+
if event.id in ep_map:
|
|
67
|
+
short_id, name, _ = ep_map[event.id]
|
|
68
|
+
ep_map[event.id] = (short_id, name, event.status)
|
|
69
|
+
|
|
70
|
+
n_failures = sum(1 if status == "failed" else 0 for _, _, status in ep_map.values())
|
|
71
|
+
if n_failures > 0:
|
|
72
|
+
output.success(f"Analysis finished, {n_failures} failed.")
|
|
73
|
+
else:
|
|
74
|
+
output.success("Analysis finished")
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
output.success("Retry queued.")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""``bevor analysis run`` — trigger analysis execution."""
|
|
2
|
+
|
|
3
|
+
from typing import Mapping, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk._sync.resources._modules import EventResource
|
|
7
|
+
from bevor_sdk.types import ID
|
|
8
|
+
from bevor_sdk.types.literals import AnalysisRunStatus
|
|
9
|
+
from bevor_sdk.types.requests.security import RunAnalysisBodyDict
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from bevor_cli import output
|
|
14
|
+
from bevor_cli.annotations.confirmations import NO_WAIT, SILENT
|
|
15
|
+
from bevor_cli.client import BevorInternalClient
|
|
16
|
+
from bevor_cli.configuration.creds import Credentials
|
|
17
|
+
from bevor_cli.configuration.state import State
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_status(ep_map: Mapping[ID, Tuple[str, str, AnalysisRunStatus]]) -> Table:
|
|
21
|
+
table = Table.grid(padding=(0, 1))
|
|
22
|
+
for k in sorted(ep_map.keys()):
|
|
23
|
+
short_id, name, status = ep_map[k]
|
|
24
|
+
table.add_row(f"Entrypoint: {name} ({short_id})", f"Status: {status}")
|
|
25
|
+
return table
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_analysis(
|
|
29
|
+
version: Optional[str] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--version",
|
|
32
|
+
help="Version id to run (default: configured analysis head).",
|
|
33
|
+
),
|
|
34
|
+
no_wait: NO_WAIT = False,
|
|
35
|
+
silent: SILENT = False,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Start a run of Bevor Security pipeline for the configured analysis head (or ``--version``)."""
|
|
38
|
+
creds = Credentials.load()
|
|
39
|
+
creds.require_token()
|
|
40
|
+
|
|
41
|
+
state = State.load()
|
|
42
|
+
workspace_id = state.validate_workspace()
|
|
43
|
+
|
|
44
|
+
wait = not no_wait
|
|
45
|
+
client = BevorInternalClient(
|
|
46
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
opts: RunAnalysisBodyDict = {}
|
|
50
|
+
if version:
|
|
51
|
+
v_ = client.analysis_versions.get(ID(version))
|
|
52
|
+
analysis_id = v_.analysis_meta.id
|
|
53
|
+
version_id = v_.id
|
|
54
|
+
opts["version_id"] = version_id
|
|
55
|
+
else:
|
|
56
|
+
analysis_id = state.validate_analysis()
|
|
57
|
+
|
|
58
|
+
run_result = client.analyses.run(analysis_id, opts, with_streaming=wait)
|
|
59
|
+
|
|
60
|
+
if wait and isinstance(run_result, EventResource):
|
|
61
|
+
ep_map = {}
|
|
62
|
+
eps = client.analyses.entrypoints(analysis_id, opts)
|
|
63
|
+
for ep in eps.results:
|
|
64
|
+
if ep.pipeline_status != "not_run" and ep.is_analyzed_in_current:
|
|
65
|
+
ep_map[ep.node.id] = (ep.node.short_id, ep.node.name, ep.pipeline_status)
|
|
66
|
+
|
|
67
|
+
if not silent:
|
|
68
|
+
with Live(render_status(ep_map), console=output.console, refresh_per_second=8) as live:
|
|
69
|
+
for event in run_result.stream_completion():
|
|
70
|
+
if event.id in ep_map:
|
|
71
|
+
short_id, name, _ = ep_map[event.id]
|
|
72
|
+
ep_map[event.id] = (short_id, name, event.status)
|
|
73
|
+
live.update(render_status(ep_map))
|
|
74
|
+
else:
|
|
75
|
+
for event in run_result.stream_completion():
|
|
76
|
+
if event.id in ep_map:
|
|
77
|
+
short_id, name, _ = ep_map[event.id]
|
|
78
|
+
ep_map[event.id] = (short_id, name, event.status)
|
|
79
|
+
|
|
80
|
+
n_failures = sum(1 if status == "failed" else 0 for _, _, status in ep_map.values())
|
|
81
|
+
if n_failures > 0:
|
|
82
|
+
output.success(f"Analysis finished, {n_failures} failed.")
|
|
83
|
+
else:
|
|
84
|
+
output.success("Analysis finished")
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
output.success("Run queued")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""``bevor analyses select`` — set the selected analysis."""
|
|
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.requests.security import AnalysesQueryParams
|
|
10
|
+
from bevor_sdk.types.responses.security import AnalysisSchema
|
|
11
|
+
|
|
12
|
+
from bevor_cli import output
|
|
13
|
+
from bevor_cli.client import BevorInternalClient
|
|
14
|
+
from bevor_cli.configuration.creds import Credentials
|
|
15
|
+
from bevor_cli.configuration.settings import Settings
|
|
16
|
+
from bevor_cli.configuration.state import State
|
|
17
|
+
from bevor_cli.select import _CANCEL, _select
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def select_analysis(
|
|
21
|
+
analysis_id: Optional[str] = typer.Argument(
|
|
22
|
+
None, help="Analysis id to select. Prompts with a list when omitted."
|
|
23
|
+
),
|
|
24
|
+
page: Optional[int] = typer.Option(None, "--page", help="Page number to query"),
|
|
25
|
+
page_size: Optional[int] = typer.Option(None, "--page-size", help="Max analyses to fetch."),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Select an analysis in the selected project by id, or pick one from a list."""
|
|
28
|
+
|
|
29
|
+
creds = Credentials.load()
|
|
30
|
+
creds.require_token()
|
|
31
|
+
|
|
32
|
+
state = State.load()
|
|
33
|
+
workspace_id = state.validate_workspace()
|
|
34
|
+
|
|
35
|
+
client = BevorInternalClient(
|
|
36
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if analysis_id:
|
|
40
|
+
try:
|
|
41
|
+
analysis_ = client.analyses.get(ID(analysis_id))
|
|
42
|
+
except NotFoundError:
|
|
43
|
+
output.error("analysis not found in the configured project")
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
if state.code_id != analysis_.version.code_version.id:
|
|
47
|
+
output.info("Updating code configuration")
|
|
48
|
+
|
|
49
|
+
state.set_analysis(analysis_)
|
|
50
|
+
state.save()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
settings = Settings.load()
|
|
54
|
+
|
|
55
|
+
analyses = client.analyses.list(
|
|
56
|
+
AnalysesQueryParams(page_size=page_size or settings.default_page_size, page=page)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not analyses.results:
|
|
60
|
+
output.error("No analyses found.", hint="Run `bevor analyses create` to create one.")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
choices = [
|
|
64
|
+
questionary.Choice(
|
|
65
|
+
title=(
|
|
66
|
+
f"{a.name} · {a.version.code_version.id} "
|
|
67
|
+
f"{a.user.username} "
|
|
68
|
+
f"{a.id} {a.created_at.date()}"
|
|
69
|
+
),
|
|
70
|
+
value=a,
|
|
71
|
+
)
|
|
72
|
+
for a in analyses.results
|
|
73
|
+
] + [questionary.Choice(title="↵ Cancel", value=_CANCEL)]
|
|
74
|
+
|
|
75
|
+
analysis: AnalysisSchema | None = _select("Select an analysis:", choices)
|
|
76
|
+
if analysis is None:
|
|
77
|
+
raise typer.Exit(0)
|
|
78
|
+
|
|
79
|
+
if analysis.version.code_version.id != state.code_id:
|
|
80
|
+
output.info("Updating code configuration")
|
|
81
|
+
|
|
82
|
+
state.set_analysis(analysis)
|
|
83
|
+
|
|
84
|
+
output.info("Updated analysis configuration")
|
|
85
|
+
state.save()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""``bevor analyses set-head`` — move HEAD to a version; keep analysis_id in config."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from bevor_sdk.types import ID
|
|
5
|
+
from bevor_sdk.types.requests.security import SetHeadBodyDict
|
|
6
|
+
|
|
7
|
+
from bevor_cli import output
|
|
8
|
+
from bevor_cli.client import BevorInternalClient
|
|
9
|
+
from bevor_cli.configuration.creds import Credentials
|
|
10
|
+
from bevor_cli.configuration.state import State
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_analysis_head(
|
|
14
|
+
version_id: str = typer.Argument(..., help="Analysis version id to set as HEAD."),
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Move the analysis HEAD to ``version_id``. This will change which analysis versions you act on."""
|
|
17
|
+
creds = Credentials.load()
|
|
18
|
+
creds.require_token()
|
|
19
|
+
|
|
20
|
+
state = State.load()
|
|
21
|
+
workspace_id = state.validate_workspace()
|
|
22
|
+
analysis_id = state.validate_analysis()
|
|
23
|
+
|
|
24
|
+
client = BevorInternalClient(
|
|
25
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
analysis_source = client.analyses(analysis_id)
|
|
29
|
+
|
|
30
|
+
analysis = analysis_source.get()
|
|
31
|
+
version = client.analysis_versions.get(ID(version_id))
|
|
32
|
+
|
|
33
|
+
if version.analysis_meta.id != analysis.id:
|
|
34
|
+
output.error(
|
|
35
|
+
f"Version {version_id} belongs to analysis {version.analysis_meta.id}, "
|
|
36
|
+
f"not the configured analysis {analysis_id}.",
|
|
37
|
+
hint="Run `bevor analyses select <analysis-id>` first, or pass a version of the configured analysis.",
|
|
38
|
+
)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
result = analysis_source.set_head(SetHeadBodyDict(version_id=ID(version_id)))
|
|
42
|
+
state.set_analysis(result)
|
|
43
|
+
state.save()
|
|
44
|
+
|
|
45
|
+
output.success(f"HEAD set to {result.version.id}. Analysis {result.id} retained in config.")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""``bevor analysis transitions``."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from bevor_sdk.types.literals import FindingTransitionReason
|
|
8
|
+
from bevor_sdk.types.requests.security import TransitionQueryParams
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
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.commands.analyses.diff import _snapshot
|
|
16
|
+
from bevor_cli.commands.findings.get import LEVEL_COLORS
|
|
17
|
+
from bevor_cli.configuration.creds import Credentials
|
|
18
|
+
from bevor_cli.configuration.settings import Settings
|
|
19
|
+
from bevor_cli.configuration.state import State
|
|
20
|
+
from bevor_cli.output import console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def list_transitions(
|
|
24
|
+
page: PAGE = None,
|
|
25
|
+
page_size: PAGE_SIZE = None,
|
|
26
|
+
order: ORDER = None,
|
|
27
|
+
order_by: ORDER_BY = None,
|
|
28
|
+
reason: Optional[FindingTransitionReason] = typer.Option(
|
|
29
|
+
None, "--reason", help="Only show transitions with this reason (e.g. remediated)."
|
|
30
|
+
),
|
|
31
|
+
version: Optional[str] = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--version",
|
|
34
|
+
help="Version id (default: configured analysis head).",
|
|
35
|
+
),
|
|
36
|
+
project_wide: bool = typer.Option(
|
|
37
|
+
False, "--project-wide", help="List transitions across the whole project instead."
|
|
38
|
+
),
|
|
39
|
+
json_out: JSON_OUT = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""List finding transitions into the configured analysis head (or ``--version``), or across the whole project
|
|
42
|
+
with ``--project-wide``. Filter with ``--reason``, e.g. ``--reason remediated``."""
|
|
43
|
+
creds = Credentials.load()
|
|
44
|
+
creds.require_token()
|
|
45
|
+
|
|
46
|
+
state = State.load()
|
|
47
|
+
settings = Settings.load()
|
|
48
|
+
workspace_id = state.validate_workspace()
|
|
49
|
+
|
|
50
|
+
client = BevorInternalClient(
|
|
51
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
query_params = TransitionQueryParams(
|
|
55
|
+
page=page,
|
|
56
|
+
page_size=page_size or settings.default_page_size,
|
|
57
|
+
order=order,
|
|
58
|
+
order_by=order_by,
|
|
59
|
+
reason=reason,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if version:
|
|
63
|
+
query_params["after_version_id"] = ID(version)
|
|
64
|
+
elif not project_wide:
|
|
65
|
+
analysis = client.analyses.get(state.validate_analysis())
|
|
66
|
+
query_params["after_version_id"] = analysis.version.id
|
|
67
|
+
|
|
68
|
+
result = client.transitions.list(options=query_params)
|
|
69
|
+
|
|
70
|
+
if json_out:
|
|
71
|
+
console.print_json(result.model_dump_json(indent=2))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if not result.results:
|
|
75
|
+
console.print("No transitions.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
table = Table("Reason", "Type", "Level", "Name", "Version", "Created")
|
|
79
|
+
for item in result.results:
|
|
80
|
+
snapshot = _snapshot(item)
|
|
81
|
+
table.add_row(
|
|
82
|
+
item.reason,
|
|
83
|
+
snapshot.type,
|
|
84
|
+
Text(snapshot.level, style=LEVEL_COLORS[snapshot.level]),
|
|
85
|
+
snapshot.name,
|
|
86
|
+
str(item.after_version.id),
|
|
87
|
+
str(item.created_at.date()),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
console.print(table)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""``bevor analyses view`` — show an analysis (the selected one by default) or a specific version."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
10
|
+
from bevor_cli.client import BevorInternalClient
|
|
11
|
+
from bevor_cli.configuration.creds import Credentials
|
|
12
|
+
from bevor_cli.configuration.state import State
|
|
13
|
+
from bevor_cli.output import console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def view_analysis(
|
|
17
|
+
analysis_id: Optional[str] = typer.Argument(
|
|
18
|
+
None, help="Analysis id. Defaults to the selected analysis."
|
|
19
|
+
),
|
|
20
|
+
version: Optional[str] = typer.Option(
|
|
21
|
+
None,
|
|
22
|
+
"--version",
|
|
23
|
+
help="Act on this analysis version id instead of the configured analysis head.",
|
|
24
|
+
),
|
|
25
|
+
json_out: JSON_OUT = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Print details for an analysis head (the selected analysis by default), or a specific ``--version``."""
|
|
28
|
+
creds = Credentials.load()
|
|
29
|
+
creds.require_token()
|
|
30
|
+
|
|
31
|
+
state = State.load()
|
|
32
|
+
workspace_id = state.validate_workspace()
|
|
33
|
+
|
|
34
|
+
client = BevorInternalClient(
|
|
35
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if version is not None:
|
|
39
|
+
version_ = client.analysis_versions.get(ID(version))
|
|
40
|
+
analysis_meta_ = version_.analysis_meta
|
|
41
|
+
else:
|
|
42
|
+
analysis = client.analyses.get(state.validate_analysis(analysis_id))
|
|
43
|
+
version_ = analysis.version
|
|
44
|
+
analysis_meta_ = analysis
|
|
45
|
+
|
|
46
|
+
if json_out:
|
|
47
|
+
console.print_json(version_.model_dump_json(indent=2))
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
in_sync = state.code_id is not None and str(state.code_id) == str(version_.code_version.id)
|
|
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("Analysis ID", str(analysis_meta_.id))
|
|
57
|
+
table.add_row("Version ID", str(version_.id))
|
|
58
|
+
table.add_row("Name", analysis_meta_.name)
|
|
59
|
+
table.add_row("Trigger", str(version_.trigger))
|
|
60
|
+
table.add_row("Owner", str(analysis_meta_.is_owner))
|
|
61
|
+
table.add_row("Findings", str(version_.n_findings))
|
|
62
|
+
table.add_row(
|
|
63
|
+
"Code version",
|
|
64
|
+
version_.code_version.id,
|
|
65
|
+
)
|
|
66
|
+
table.add_row("Is head", str(version_.is_head))
|
|
67
|
+
table.add_row("Created at", str(version_.created_at.date()))
|
|
68
|
+
table.add_row("Created by", analysis_meta_.user.username)
|
|
69
|
+
|
|
70
|
+
if version_.merged_from_version:
|
|
71
|
+
table.add_row(
|
|
72
|
+
"Merged from",
|
|
73
|
+
f"{version_.merged_from_version.id}",
|
|
74
|
+
)
|
|
75
|
+
if version_.parent_version:
|
|
76
|
+
table.add_row(
|
|
77
|
+
"Parent version",
|
|
78
|
+
f"{version_.parent_version.id}",
|
|
79
|
+
)
|
|
80
|
+
if version_.forked_from_version:
|
|
81
|
+
table.add_row(
|
|
82
|
+
"Forked from",
|
|
83
|
+
f"{version_.forked_from_version.id}",
|
|
84
|
+
)
|
|
85
|
+
if version_.child_versions:
|
|
86
|
+
table.add_row("N Children", str(len(version_.child_versions)))
|
|
87
|
+
if version_.forked_versions:
|
|
88
|
+
table.add_row("N Forks", str(len(version_.forked_versions)))
|
|
89
|
+
if version_.merged_versions:
|
|
90
|
+
table.add_row("N Merges From", str(len(version_.merged_versions)))
|
|
91
|
+
|
|
92
|
+
table.add_row("In sync w/ .bevor code", str(in_sync))
|
|
93
|
+
|
|
94
|
+
console.print(table)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""``bevor login``."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import secrets
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
from queue import Empty, Queue
|
|
9
|
+
from typing import List
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from bevor_cli import output
|
|
16
|
+
from bevor_cli.client import BevorInternalBusinessClient
|
|
17
|
+
from bevor_cli.configuration.creds import Credentials
|
|
18
|
+
from bevor_cli.settings import API_URL, APP_URL
|
|
19
|
+
|
|
20
|
+
_LISTEN_TIMEOUT_S = 360.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cli_client_ip() -> str:
|
|
24
|
+
try:
|
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
26
|
+
s.settimeout(0.2)
|
|
27
|
+
s.connect(("8.8.8.8", 80))
|
|
28
|
+
return s.getsockname()[0]
|
|
29
|
+
except OSError:
|
|
30
|
+
return "127.0.0.1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sse_listen(token: str, result: Queue) -> None:
|
|
34
|
+
url = f"{API_URL.rstrip('/')}/v1/events/cli?token={token}"
|
|
35
|
+
try:
|
|
36
|
+
timeout = httpx.Timeout(_LISTEN_TIMEOUT_S + 30.0, connect=30.0)
|
|
37
|
+
with httpx.Client(timeout=timeout) as client:
|
|
38
|
+
with client.stream("GET", url) as response:
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
pending: List[str] = []
|
|
41
|
+
for line in response.iter_lines():
|
|
42
|
+
if line == "":
|
|
43
|
+
for row in pending:
|
|
44
|
+
if row.startswith("data: "):
|
|
45
|
+
payload = json.loads(row[6:])
|
|
46
|
+
t = payload.get("type")
|
|
47
|
+
if t == "authorized":
|
|
48
|
+
result.put(("ok", payload))
|
|
49
|
+
return
|
|
50
|
+
if t == "closed":
|
|
51
|
+
result.put(("closed", payload))
|
|
52
|
+
return
|
|
53
|
+
pending = []
|
|
54
|
+
continue
|
|
55
|
+
if line.startswith(":"):
|
|
56
|
+
continue
|
|
57
|
+
pending.append(line)
|
|
58
|
+
result.put(("closed", {"reason": "stream_ended"}))
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
result.put(("error", exc))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def login() -> None:
|
|
64
|
+
"""Log in via browser.
|
|
65
|
+
|
|
66
|
+
Opens your default browser to authenticate with Bevor. Your credentials
|
|
67
|
+
are stored locally and used for subsequent commands.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
creds = Credentials.load()
|
|
71
|
+
|
|
72
|
+
if creds.is_authenticated:
|
|
73
|
+
output.info("Already authenticated.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
session_id = secrets.token_urlsafe(24)
|
|
77
|
+
auth_url = f"{APP_URL.rstrip('/')}/cli-auth?{urlencode({'session_id': session_id})}"
|
|
78
|
+
|
|
79
|
+
device_name = socket.gethostname() or "unknown"
|
|
80
|
+
os_name = platform.platform()
|
|
81
|
+
ip_address = _cli_client_ip()
|
|
82
|
+
|
|
83
|
+
client = BevorInternalBusinessClient(creds=creds)
|
|
84
|
+
|
|
85
|
+
exchange_data = client.post(
|
|
86
|
+
"/v1/cli-tokens/exchange",
|
|
87
|
+
json={
|
|
88
|
+
"session_id": session_id,
|
|
89
|
+
"ip_address": ip_address,
|
|
90
|
+
"device_name": device_name,
|
|
91
|
+
"os_name": os_name,
|
|
92
|
+
},
|
|
93
|
+
).json()
|
|
94
|
+
|
|
95
|
+
token = exchange_data["token"]
|
|
96
|
+
|
|
97
|
+
result: Queue = Queue(maxsize=1)
|
|
98
|
+
thread = threading.Thread(
|
|
99
|
+
target=_sse_listen,
|
|
100
|
+
args=(token, result),
|
|
101
|
+
daemon=True,
|
|
102
|
+
)
|
|
103
|
+
thread.start()
|
|
104
|
+
|
|
105
|
+
output.console.print(
|
|
106
|
+
f"If the browser does not open automatically, visit [bold blue link {auth_url}]{auth_url}[/]!"
|
|
107
|
+
)
|
|
108
|
+
typer.launch(auth_url)
|
|
109
|
+
|
|
110
|
+
with output.console.status("Waiting for login (complete in the browser)...", spinner="dots"):
|
|
111
|
+
try:
|
|
112
|
+
kind, payload = result.get(timeout=_LISTEN_TIMEOUT_S)
|
|
113
|
+
except Empty:
|
|
114
|
+
output.error("Timed out waiting for login.")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
|
|
117
|
+
if kind == "error":
|
|
118
|
+
output.error(f"SSE error: {payload}")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
if kind == "closed":
|
|
121
|
+
reason = payload.get("reason", "")
|
|
122
|
+
msg = "Login cancelled or closed."
|
|
123
|
+
if reason:
|
|
124
|
+
msg += f" ({reason})"
|
|
125
|
+
output.error(msg)
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
raw = payload["token"]
|
|
129
|
+
bearer = raw if raw.startswith("cli_") else f"cli_{raw}"
|
|
130
|
+
creds = Credentials.save(token=bearer)
|
|
131
|
+
|
|
132
|
+
client = BevorInternalBusinessClient(creds=creds)
|
|
133
|
+
user = client.me()
|
|
134
|
+
|
|
135
|
+
output.success(f"Logged in as {user.email or user.username}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""``bevor logout``."""
|
|
2
|
+
|
|
3
|
+
from bevor_cli import output
|
|
4
|
+
from bevor_cli.client import BevorInternalBusinessClient
|
|
5
|
+
from bevor_cli.configuration.creds import Credentials
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def logout() -> None:
|
|
9
|
+
"""Remove stored credentials."""
|
|
10
|
+
creds = Credentials.load()
|
|
11
|
+
|
|
12
|
+
if not creds.is_authenticated:
|
|
13
|
+
output.info("Not currently authenticated")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
client = BevorInternalBusinessClient(creds=creds)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
client.delete("/v1/cli-tokens/revoke/current")
|
|
20
|
+
except Exception:
|
|
21
|
+
pass # still clear local credentials
|
|
22
|
+
creds.delete()
|
|
23
|
+
output.success("Logged out.")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""``bevor whoami``."""
|
|
2
|
+
|
|
3
|
+
from bevor_cli import output
|
|
4
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
5
|
+
from bevor_cli.client import BevorInternalBusinessClient
|
|
6
|
+
from bevor_cli.configuration.creds import Credentials
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def whoami(json_out: JSON_OUT = False) -> None:
|
|
10
|
+
"""Show the currently authenticated user."""
|
|
11
|
+
creds = Credentials.load()
|
|
12
|
+
creds.require_token()
|
|
13
|
+
|
|
14
|
+
client = BevorInternalBusinessClient(creds=creds)
|
|
15
|
+
|
|
16
|
+
user = client.me()
|
|
17
|
+
|
|
18
|
+
if json_out:
|
|
19
|
+
output.console.print_json(user.model_dump_json(indent=2))
|
|
20
|
+
else:
|
|
21
|
+
output.console.print(user.email or user.username)
|
|
File without changes
|