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.
Files changed (100) hide show
  1. bevor_cli/__init__.py +0 -0
  2. bevor_cli/annotations/__init__.py +0 -0
  3. bevor_cli/annotations/confirmations.py +16 -0
  4. bevor_cli/annotations/query.py +25 -0
  5. bevor_cli/apps/agent.py +10 -0
  6. bevor_cli/apps/analyses.py +47 -0
  7. bevor_cli/apps/chat.py +16 -0
  8. bevor_cli/apps/codes.py +18 -0
  9. bevor_cli/apps/findings.py +22 -0
  10. bevor_cli/apps/main.py +61 -0
  11. bevor_cli/apps/nodes.py +17 -0
  12. bevor_cli/apps/projects.py +14 -0
  13. bevor_cli/apps/sessions.py +13 -0
  14. bevor_cli/apps/workspaces.py +16 -0
  15. bevor_cli/client.py +157 -0
  16. bevor_cli/commands/__init__.py +0 -0
  17. bevor_cli/commands/agent/__init__.py +0 -0
  18. bevor_cli/commands/agent/materialze_cmd.py +65 -0
  19. bevor_cli/commands/agent/setup_cmd.py +18 -0
  20. bevor_cli/commands/analyses/__init__.py +0 -0
  21. bevor_cli/commands/analyses/active.py +42 -0
  22. bevor_cli/commands/analyses/chats/__init__.py +0 -0
  23. bevor_cli/commands/analyses/chats/get.py +39 -0
  24. bevor_cli/commands/analyses/chats/list.py +64 -0
  25. bevor_cli/commands/analyses/chats/messages.py +41 -0
  26. bevor_cli/commands/analyses/chats/new.py +44 -0
  27. bevor_cli/commands/analyses/chats/resume.py +43 -0
  28. bevor_cli/commands/analyses/chats/session.py +295 -0
  29. bevor_cli/commands/analyses/child.py +81 -0
  30. bevor_cli/commands/analyses/commit.py +49 -0
  31. bevor_cli/commands/analyses/create.py +105 -0
  32. bevor_cli/commands/analyses/delete.py +65 -0
  33. bevor_cli/commands/analyses/diff.py +87 -0
  34. bevor_cli/commands/analyses/entrypoints.py +56 -0
  35. bevor_cli/commands/analyses/findings.py +129 -0
  36. bevor_cli/commands/analyses/fork.py +47 -0
  37. bevor_cli/commands/analyses/list.py +85 -0
  38. bevor_cli/commands/analyses/merge.py +46 -0
  39. bevor_cli/commands/analyses/retry.py +77 -0
  40. bevor_cli/commands/analyses/run.py +87 -0
  41. bevor_cli/commands/analyses/select.py +85 -0
  42. bevor_cli/commands/analyses/set_head.py +45 -0
  43. bevor_cli/commands/analyses/transitions.py +90 -0
  44. bevor_cli/commands/analyses/view.py +94 -0
  45. bevor_cli/commands/auth/__init__.py +7 -0
  46. bevor_cli/commands/auth/login.py +135 -0
  47. bevor_cli/commands/auth/logout.py +23 -0
  48. bevor_cli/commands/auth/whoami.py +21 -0
  49. bevor_cli/commands/codes/__init__.py +0 -0
  50. bevor_cli/commands/codes/list.py +90 -0
  51. bevor_cli/commands/codes/push.py +76 -0
  52. bevor_cli/commands/codes/select.py +93 -0
  53. bevor_cli/commands/codes/view.py +80 -0
  54. bevor_cli/commands/context/__init__.py +92 -0
  55. bevor_cli/commands/findings/__init__.py +0 -0
  56. bevor_cli/commands/findings/acknowledge.py +28 -0
  57. bevor_cli/commands/findings/add.py +236 -0
  58. bevor_cli/commands/findings/bulk.py +110 -0
  59. bevor_cli/commands/findings/delete.py +28 -0
  60. bevor_cli/commands/findings/edit.py +109 -0
  61. bevor_cli/commands/findings/get.py +62 -0
  62. bevor_cli/commands/findings/interactive.py +329 -0
  63. bevor_cli/commands/findings/list.py +108 -0
  64. bevor_cli/commands/findings/revert.py +28 -0
  65. bevor_cli/commands/graph/__init__.py +0 -0
  66. bevor_cli/commands/graph/call_chain.py +48 -0
  67. bevor_cli/commands/graph/edges.py +59 -0
  68. bevor_cli/commands/graph/interactive.py +207 -0
  69. bevor_cli/commands/graph/renderer.py +135 -0
  70. bevor_cli/commands/link/__init__.py +52 -0
  71. bevor_cli/commands/link/helpers.py +26 -0
  72. bevor_cli/commands/nodes/__init__.py +0 -0
  73. bevor_cli/commands/nodes/get.py +52 -0
  74. bevor_cli/commands/nodes/list.py +75 -0
  75. bevor_cli/commands/nodes/source.py +40 -0
  76. bevor_cli/commands/projects/__init__.py +0 -0
  77. bevor_cli/commands/projects/create.py +54 -0
  78. bevor_cli/commands/projects/list.py +57 -0
  79. bevor_cli/commands/projects/select.py +75 -0
  80. bevor_cli/commands/projects/view.py +49 -0
  81. bevor_cli/commands/sessions/__init__.py +0 -0
  82. bevor_cli/commands/sessions/list.py +36 -0
  83. bevor_cli/commands/sessions/revoke.py +35 -0
  84. bevor_cli/commands/workspaces/__init__.py +0 -0
  85. bevor_cli/commands/workspaces/list.py +50 -0
  86. bevor_cli/commands/workspaces/pick.py +20 -0
  87. bevor_cli/commands/workspaces/select.py +68 -0
  88. bevor_cli/commands/workspaces/view.py +48 -0
  89. bevor_cli/configuration/creds.py +52 -0
  90. bevor_cli/configuration/settings.py +54 -0
  91. bevor_cli/configuration/state.py +113 -0
  92. bevor_cli/findings_yaml.py +291 -0
  93. bevor_cli/main.py +33 -0
  94. bevor_cli/output.py +69 -0
  95. bevor_cli/select.py +64 -0
  96. bevor_cli/settings.py +4 -0
  97. bevor_cli-0.1.0.dist-info/METADATA +14 -0
  98. bevor_cli-0.1.0.dist-info/RECORD +100 -0
  99. bevor_cli-0.1.0.dist-info/WHEEL +4 -0
  100. bevor_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,64 @@
1
+ """``bevor analysis chats list``."""
2
+
3
+ from typing import Annotated, Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types.requests.chat import ChatsQueryParams
7
+ from rich.table import Table
8
+
9
+ from bevor_cli.annotations.confirmations import JSON_OUT
10
+ from bevor_cli.annotations.query import ORDER, ORDER_BY, PAGE, PAGE_SIZE
11
+ from bevor_cli.client import BevorInternalClient
12
+ from bevor_cli.configuration.creds import Credentials
13
+ from bevor_cli.configuration.settings import Settings
14
+ from bevor_cli.configuration.state import State
15
+ from bevor_cli.output import console
16
+
17
+
18
+ def list_chats(
19
+ page: PAGE,
20
+ page_size: PAGE_SIZE,
21
+ order: ORDER,
22
+ order_by: ORDER_BY,
23
+ project: Annotated[Optional[str], typer.Option("--project", help="project to list chats for")],
24
+ json_out: JSON_OUT = False,
25
+ ) -> None:
26
+ """List chats. Uses the currently configured project, or --project"""
27
+ creds = Credentials.load()
28
+ creds.require_token()
29
+
30
+ state = State.load()
31
+ settings = Settings.load()
32
+ workspace_id = state.validate_workspace()
33
+ client = BevorInternalClient(
34
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project(priority=project)
35
+ )
36
+
37
+ query_params = ChatsQueryParams(
38
+ page=page,
39
+ page_size=page_size or settings.default_page_size,
40
+ order=order,
41
+ order_by=order_by,
42
+ )
43
+
44
+ found = client.chats.list(options=query_params)
45
+
46
+ if json_out:
47
+ console.print_json(found.model_dump_json(indent=2))
48
+ return
49
+
50
+ if not found.results:
51
+ console.print("No chats found.")
52
+ return
53
+
54
+ table = Table("ID", "Title", "Code", "Version", "Created")
55
+ for c in found.results:
56
+ table.add_row(
57
+ str(c.id),
58
+ c.title or "",
59
+ str(c.code_id),
60
+ str(c.version_id or ""),
61
+ str(c.created_at.date()),
62
+ )
63
+
64
+ console.print(table)
@@ -0,0 +1,41 @@
1
+ """``bevor analyses chats messages``."""
2
+
3
+ import json
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+
8
+ from bevor_cli.annotations.confirmations import JSON_OUT
9
+ from bevor_cli.client import BevorInternalClient
10
+ from bevor_cli.configuration.creds import Credentials
11
+ from bevor_cli.configuration.state import State
12
+ from bevor_cli.output import console
13
+
14
+
15
+ def chat_messages(
16
+ chat_id: str = typer.Argument(..., help="Chat ID."),
17
+ json_out: JSON_OUT = False,
18
+ ) -> None:
19
+ """Get chat messages for a chat."""
20
+ creds = Credentials.load()
21
+ creds.require_token()
22
+
23
+ state = State.load()
24
+ workspace_id = state.validate_workspace()
25
+
26
+ client = BevorInternalClient(
27
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
28
+ )
29
+
30
+ messages = client.chats.messages(ID(chat_id))
31
+
32
+ if json_out:
33
+ console.print_json(
34
+ json.dumps([m.model_dump(mode="json") for m in messages.results], indent=2)
35
+ )
36
+ return
37
+
38
+ for m in messages.results:
39
+ role = m.chat_role or ""
40
+ text = m.message or ""
41
+ console.print(f"[{str(role):>9}] {text[:120]}")
@@ -0,0 +1,44 @@
1
+ from typing import Literal
2
+
3
+ import typer
4
+
5
+ from bevor_cli.client import BevorInternalClient
6
+ from bevor_cli.commands.analyses.chats.session import _open_streaming_session
7
+ from bevor_cli.configuration.creds import Credentials
8
+ from bevor_cli.configuration.state import State
9
+
10
+
11
+ def chat_new(
12
+ mode: Literal["analysis", "code"] = typer.Argument(
13
+ ..., help="whether to create a chat with analysis or code"
14
+ ),
15
+ ) -> None:
16
+ """Create a chat thread and open the streaming terminal session. The mode determines if you want
17
+ to chat with just code, or with analysis + code. Either use your current configuration."""
18
+
19
+ creds = Credentials.load()
20
+ creds.require_token()
21
+
22
+ state = State.load()
23
+ workspace_id = state.validate_workspace()
24
+
25
+ client = BevorInternalClient(
26
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
27
+ )
28
+
29
+ if mode == "analysis":
30
+ analysis_id = state.validate_analysis()
31
+ analysis = client.analyses.get(analysis_id)
32
+ created = client.chats.create_for_analysis(version_id=analysis.version.id)
33
+ _open_streaming_session(
34
+ client,
35
+ analysis_id=analysis_id,
36
+ version_id=analysis.version.id,
37
+ thread_id=created.id,
38
+ )
39
+ elif mode == "code":
40
+ code_id = state.validate_code()
41
+ created = client.chats.create_for_code(code_id=code_id)
42
+ _open_streaming_session(client, analysis_id=None, thread_id=created.id)
43
+ else:
44
+ raise typer.Exit(1)
@@ -0,0 +1,43 @@
1
+ """``bevor chats`` — interactive streaming chat for the active security analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from bevor_cli import output
8
+ from bevor_cli.client import BevorInternalClient
9
+ from bevor_cli.commands.analyses.chats.session import (
10
+ _ensure_thread_matches_analysis,
11
+ _open_streaming_session,
12
+ )
13
+ from bevor_cli.configuration.creds import Credentials
14
+ from bevor_cli.configuration.state import State
15
+
16
+
17
+ def chat_resume(
18
+ chat_id: str = typer.Argument(
19
+ ...,
20
+ metavar="CHAT_ID",
21
+ help="Thread id (`bevor chats list --json`, field id).",
22
+ ),
23
+ ) -> None:
24
+ """Open the streaming session for an existing thread."""
25
+ creds = Credentials.load()
26
+ creds.require_token()
27
+
28
+ state = State.load()
29
+ workspace_id = state.validate_workspace()
30
+
31
+ if not state.analysis_id:
32
+ output.error(
33
+ "No analysis is configured.",
34
+ hint="Run `bevor analyses select` or `bevor analyses create`.",
35
+ )
36
+ raise typer.Exit(1)
37
+
38
+ client = BevorInternalClient(
39
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
40
+ )
41
+ tid = chat_id.strip()
42
+ _ensure_thread_matches_analysis(client, tid, state.analysis_id)
43
+ _open_streaming_session(client, analysis_id=state.analysis_id, thread_id=tid)
@@ -0,0 +1,295 @@
1
+ """``bevor chats`` — interactive streaming chat for the active security analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import itertools
6
+ import sys
7
+ import threading
8
+ from typing import Any, Dict, List
9
+
10
+ import httpx
11
+ import questionary
12
+ import typer
13
+ from bevor_sdk._sync.resources.chat.chats import BoundChatResources
14
+ from bevor_sdk.types import ID
15
+ from rich.console import Console, ConsoleOptions, RenderResult
16
+ from rich.live import Live
17
+ from rich.markdown import CodeBlock, Markdown
18
+ from rich.panel import Panel
19
+ from rich.text import Text
20
+
21
+ from bevor_cli import output
22
+ from bevor_cli.client import BevorInternalClient
23
+
24
+ _CHAT_STYLE = questionary.Style(
25
+ [
26
+ ("highlighted", "bold"),
27
+ ("selected", "bold fg:ansibrightyellow"),
28
+ ("instruction", "fg:ansibrightblack italic"),
29
+ ("pointer", "bold fg:ansibrightyellow"),
30
+ ]
31
+ )
32
+
33
+ _STREAM_HTTP_TIMEOUT = httpx.Timeout(600.0, connect=60.0)
34
+
35
+
36
+ def _recv_spinner(stop: threading.Event) -> None:
37
+ frames = itertools.cycle("⠋⠙⠹⠸⠼⠴⠦⠇⠏")
38
+ while not stop.is_set():
39
+ sys.stdout.write(f"\r\033[2K{next(frames)} Receiving…")
40
+ sys.stdout.flush()
41
+ stop.wait(0.09)
42
+ sys.stdout.write("\r\033[2K")
43
+ sys.stdout.flush()
44
+
45
+
46
+ def _stream_fence_buffer(content: str) -> str:
47
+ """Synthetic closing fence for render only (same rule as web ``setBuffer('\\n```')`` when odd)."""
48
+ if content.count("```") % 2 != 0:
49
+ return "\n```"
50
+ return ""
51
+
52
+
53
+ def _stream_markdown_markup(streamed_content: str) -> str:
54
+ """What the UI renders: ``streamedContent + buffer``."""
55
+ return streamed_content + _stream_fence_buffer(streamed_content)
56
+
57
+
58
+ class _PlainCodeBlock(CodeBlock):
59
+ """Fenced bodies during SSE: monospace wrap only — Rich's default uses Pygments and stalls."""
60
+
61
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
62
+ code = str(self.text).rstrip()
63
+ yield Text(code, style=self.style_name, overflow="fold")
64
+
65
+
66
+ class _StreamingLiveMarkdown(Markdown):
67
+ elements = {**Markdown.elements, "code_block": _PlainCodeBlock}
68
+
69
+
70
+ def _stream_markdown_renderable(markup: str) -> Markdown:
71
+ return _StreamingLiveMarkdown(markup)
72
+
73
+
74
+ def _run_stream_turn(
75
+ bound: BoundChatResources,
76
+ *,
77
+ http: httpx.Client,
78
+ message: str,
79
+ approval_id: str | None,
80
+ is_approved: bool | None,
81
+ ) -> tuple[List[str], str | None, str | None]:
82
+ """Consume one POST /stream response.
83
+
84
+ Returns ``([], approval_tool_id, approval_prompt)`` — reference events are ignored in the CLI.
85
+
86
+ Text events mirror the web client: ``streamedContent`` is the raw payload; for Markdown we append a
87
+ closing-fence buffer when ``` count is odd. Live updates use markdown with plain fenced bodies so
88
+ Pygments is not invoked on every chunk (that was freezing the stream once a fence opened).
89
+ """
90
+ kw: Dict[str, Any] = {
91
+ "message": message,
92
+ "client": http,
93
+ }
94
+ if approval_id is not None:
95
+ kw["approval_id"] = approval_id
96
+ kw["is_approved"] = bool(is_approved)
97
+
98
+ references: List[str] = []
99
+ pending_tool: str | None = None
100
+ pending_prompt: str | None = None
101
+
102
+ stream_live: Live | None = None
103
+ live_text_active = False
104
+ final_stream_text: str | None = None
105
+
106
+ spinner_stop = threading.Event()
107
+ spinner_thread: threading.Thread | None = None
108
+
109
+ def stop_recv_spinner() -> None:
110
+ nonlocal spinner_thread
111
+ spinner_stop.set()
112
+ if spinner_thread is not None:
113
+ spinner_thread.join(timeout=1.0)
114
+ spinner_thread = None
115
+
116
+ def stop_stream_live() -> None:
117
+ nonlocal stream_live
118
+ if stream_live is not None:
119
+ stream_live.stop()
120
+ stream_live = None
121
+
122
+ def flush_streamed_markdown(*, quiet_plain_fallback: bool) -> None:
123
+ nonlocal live_text_active, final_stream_text
124
+ stop_stream_live()
125
+ if quiet_plain_fallback and final_stream_text is not None:
126
+ output.console.print(
127
+ Markdown(_stream_markdown_markup(final_stream_text)),
128
+ end="\n",
129
+ )
130
+ live_text_active = False
131
+ final_stream_text = None
132
+
133
+ output.console.print("\n[bold cyan]Assistant[/bold cyan]")
134
+ sys.stdout.flush()
135
+
136
+ if not output.is_quiet():
137
+ spinner_thread = threading.Thread(
138
+ target=_recv_spinner,
139
+ args=(spinner_stop,),
140
+ daemon=True,
141
+ )
142
+ assert spinner_thread
143
+ spinner_thread.start()
144
+
145
+ try:
146
+ for obj in bound.stream(**kw):
147
+ if not obj:
148
+ continue
149
+ stop_recv_spinner()
150
+
151
+ if obj.event_type == "text":
152
+ raw = str(obj.content or "")
153
+ final_stream_text = raw
154
+ markup = _stream_markdown_markup(raw)
155
+ if output.is_quiet():
156
+ live_text_active = True
157
+ continue
158
+ md = _stream_markdown_renderable(markup)
159
+ if stream_live is None:
160
+ stream_live = Live(
161
+ md,
162
+ console=output.console,
163
+ refresh_per_second=15,
164
+ transient=False,
165
+ vertical_overflow="visible",
166
+ redirect_stdout=False,
167
+ redirect_stderr=False,
168
+ )
169
+ assert stream_live
170
+ stream_live.start()
171
+ else:
172
+ stream_live.update(md)
173
+ live_text_active = True
174
+ continue
175
+
176
+ if live_text_active:
177
+ flush_streamed_markdown(quiet_plain_fallback=output.is_quiet())
178
+
179
+ if obj.event_type == "thinking":
180
+ output.console.print(f"[dim italic]{obj.content or 'thinking…'}[/dim italic]")
181
+ continue
182
+
183
+ if obj.event_type == "tool-call":
184
+ output.console.print(f"[dim]tool[/dim] [yellow]{obj.content}[/yellow]")
185
+ continue
186
+
187
+ if obj.event_type == "approval":
188
+ pending_tool = obj.id
189
+ pending_prompt = obj.content or "Approve this tool call?"
190
+ break
191
+
192
+ if obj.event_type == "failure":
193
+ output.console.print(f"[bold red]{obj.content or 'Request failed'}[/bold red]")
194
+ break
195
+
196
+ if obj.event_type == "reference":
197
+ continue
198
+
199
+ if live_text_active:
200
+ flush_streamed_markdown(quiet_plain_fallback=output.is_quiet())
201
+ finally:
202
+ stop_recv_spinner()
203
+
204
+ return references, pending_tool, pending_prompt
205
+
206
+
207
+ def _session_banner(*, analysis_id: str | None, version_id: str | None, thread_id: str) -> None:
208
+ context = ""
209
+ if analysis_id:
210
+ context += f"[bold]Analysis[/bold] {analysis_id}\n"
211
+ if version_id:
212
+ context += f"[bold]Version[/bold] {version_id}\n"
213
+ output.console.print(
214
+ Panel.fit(
215
+ f"{context}[bold]Thread[/bold] {thread_id}\n\n"
216
+ "[dim]/quit · /exit · Ctrl+D · requires synced code (`bevor codes push`)[/dim]",
217
+ title="Bevor Chat",
218
+ border_style="blue",
219
+ )
220
+ )
221
+
222
+
223
+ def _ensure_thread_matches_analysis(security: BevorInternalClient, thread_id: str, aid: str) -> None:
224
+ chat = security.chats.get(ID(thread_id))
225
+ if chat.version_id is not None and str(chat.version_id) != aid:
226
+ output.error(
227
+ f"Chat {thread_id} belongs to another analysis ({chat.version_id}).",
228
+ hint="Pick a thread from `bevor chats list` or run `bevor analyses select`.",
229
+ )
230
+ raise typer.Exit(1)
231
+
232
+
233
+ def _open_streaming_session(
234
+ security: BevorInternalClient,
235
+ *,
236
+ analysis_id: str | None,
237
+ version_id: str | None = None,
238
+ thread_id: str,
239
+ ) -> None:
240
+ _session_banner(analysis_id=analysis_id, version_id=version_id, thread_id=thread_id)
241
+ try:
242
+ _interactive_loop(security, thread_id=thread_id)
243
+ except KeyboardInterrupt:
244
+ output.console.print("\n[dim]Interrupted.[/dim]")
245
+
246
+
247
+ def _interactive_loop(client: BevorInternalClient, *, thread_id: str) -> None:
248
+ bound = client.chats(ID(thread_id))
249
+
250
+ with httpx.Client(timeout=_STREAM_HTTP_TIMEOUT) as http:
251
+ while True:
252
+ user_line = questionary.text(
253
+ "You ›",
254
+ qmark="",
255
+ style=_CHAT_STYLE,
256
+ ).ask()
257
+
258
+ if user_line is None:
259
+ output.console.print("[dim]Goodbye.[/dim]")
260
+ break
261
+
262
+ text = user_line.strip()
263
+ if not text:
264
+ continue
265
+ low = text.lower()
266
+ if low in ("/quit", "/exit", ":q", "exit", "quit"):
267
+ output.console.print("[dim]Goodbye.[/dim]")
268
+ break
269
+
270
+ approval_id: str | None = None
271
+ is_approved: bool | None = None
272
+
273
+ while True:
274
+ refs, pending_tool, pending_prompt = _run_stream_turn(
275
+ bound,
276
+ http=http,
277
+ message=text,
278
+ approval_id=approval_id,
279
+ is_approved=is_approved,
280
+ )
281
+
282
+ if pending_tool:
283
+ choice = questionary.confirm(
284
+ pending_prompt or "Approve tool execution?",
285
+ default=False,
286
+ style=_CHAT_STYLE,
287
+ ).ask()
288
+ approved = bool(choice) if choice is not None else False
289
+ approval_id = pending_tool
290
+ is_approved = approved
291
+ continue
292
+
293
+ for row in refs:
294
+ output.console.print(row)
295
+ break
@@ -0,0 +1,81 @@
1
+ """``bevor analysis child`` — create a child version on new code."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.security import CreateAnalysisChildBodyDict
8
+ from bevor_sdk.types.responses.security import AnalysisCollisionResult, AnalysisCreatedResult
9
+ from rich.table import Table
10
+
11
+ from bevor_cli import output
12
+ from bevor_cli.client import BevorInternalClient
13
+ from bevor_cli.configuration.creds import Credentials
14
+ from bevor_cli.configuration.state import State
15
+ from bevor_cli.output import console
16
+
17
+
18
+ def create_child_analysis(
19
+ version: Optional[str] = typer.Option(
20
+ None,
21
+ "--version",
22
+ help="Parent version id (default: configured analysis head).",
23
+ ),
24
+ collision: Optional[Literal["surface", "force", "merge"]] = typer.Option(
25
+ "surface",
26
+ "--collision",
27
+ help="What Bevor should do if analysis already exists for the given code",
28
+ ),
29
+ ) -> None:
30
+ """Create a child analysis version using the configured code. If there is no currently configured
31
+ analysis (`bevor context` or `bevor analyses view`), you can pass a --version, or you can select an analysis
32
+ using `bevor analyses select`. Bevor will check whether an analysis
33
+ exists for the given code version or not, and early return if so. Use the --collision argument to bypass this,
34
+ but it's advised not to."""
35
+
36
+ creds = Credentials.load()
37
+ creds.require_token()
38
+
39
+ state = State.load()
40
+ workspace_id = state.validate_workspace()
41
+ code_id = state.validate_code()
42
+
43
+ client = BevorInternalClient(
44
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
45
+ )
46
+
47
+ body = CreateAnalysisChildBodyDict(code_id=code_id, on_collision=collision)
48
+ if version:
49
+ v_ = client.analysis_versions.get(ID(version))
50
+ analysis_id = v_.analysis_meta.id
51
+ version_id = v_.id
52
+ body["version_id"] = version_id
53
+ else:
54
+ analysis_id = state.validate_analysis()
55
+
56
+ created = client.analyses.create_child(analysis_id, body)
57
+
58
+ if isinstance(created, AnalysisCreatedResult):
59
+ state.set_analysis(created.analysis)
60
+ state.save()
61
+
62
+ output.success(
63
+ f"Child analysis version created: {created.analysis.version.id} "
64
+ f"(analysis {created.analysis.id})."
65
+ )
66
+ elif isinstance(created, AnalysisCollisionResult):
67
+ output.warning(
68
+ f"{len(created.candidates)} analysis versions already exist for this code version. They're listed out with the proposed actions. "
69
+ "if you want to create the analysis version anyways, use the --collision=force flag."
70
+ )
71
+ table = Table("ID", "Name", "Created", "User", "Action")
72
+ for candidate in created.candidates:
73
+ args = [
74
+ candidate.id,
75
+ candidate.analysis_meta.name,
76
+ str(candidate.created_at.date()),
77
+ candidate.analysis_meta.user.username,
78
+ candidate.on_collision,
79
+ ]
80
+ table.add_row(*args)
81
+ console.print(table)
@@ -0,0 +1,49 @@
1
+ """``bevor analysis commit`` — commit staged finding changes."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.security import VersionBodyDict
8
+
9
+ from bevor_cli import output
10
+ from bevor_cli.client import BevorInternalClient
11
+ from bevor_cli.configuration.creds import Credentials
12
+ from bevor_cli.configuration.state import State
13
+
14
+
15
+ def commit_analysis(
16
+ version: Optional[str] = typer.Option(
17
+ None,
18
+ "--version",
19
+ help="Version id to commit (default: configured analysis head).",
20
+ ),
21
+ ) -> None:
22
+ """Commit staged findings for an analysis. This will update the HEAD of the configured analysis, or of the
23
+ analysis associated to the --version."""
24
+ creds = Credentials.load()
25
+ creds.require_token()
26
+
27
+ state = State.load()
28
+ workspace_id = state.validate_workspace()
29
+ analysis_id = state.validate_analysis()
30
+
31
+ client = BevorInternalClient(
32
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
33
+ )
34
+
35
+ body = VersionBodyDict()
36
+ if version:
37
+ v_ = client.analysis_versions.get(ID(version))
38
+ analysis_id = v_.analysis_meta.id
39
+ version_id = v_.id
40
+ body["version_id"] = version_id
41
+ else:
42
+ analysis_id = state.validate_analysis()
43
+
44
+ result = client.analyses.commit(analysis_id, body)
45
+
46
+ state.set_analysis(result)
47
+ state.save()
48
+
49
+ output.success(f"Committed changes → new head {result.version.id} (analysis {result.id}).")