Skip to content

Commit 1948204

Browse files
committed
cli: add CI integration command
1 parent 28efd7d commit 1948204

6 files changed

Lines changed: 304 additions & 0 deletions

File tree

onekey_client/cli/ci.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import sys
2+
import time
3+
from uuid import UUID
4+
5+
import click
6+
import httpx
7+
8+
from onekey_client import Client
9+
from onekey_client.queries import load_query
10+
11+
FIRMWARE_STATUS_QUERY = load_query("get_firmware_latest_analysis_state.graphql")
12+
GET_ALL_FIRMWARES = load_query("get_same_product_firmwares.graphql")
13+
COMPARE_FIRMWARE = load_query("compare_firmware.graphql")
14+
LATEST_ISSUES_QUERY = load_query("get_firmware_latest_results.graphql")
15+
16+
17+
class ResultHandler:
18+
def __init__(
19+
self,
20+
client: Client,
21+
firmware_id: UUID,
22+
retry_count=10,
23+
retry_wait=60,
24+
check_interval=60,
25+
):
26+
self.client = client
27+
self.firmware_id = str(firmware_id)
28+
self.retry_count = retry_count
29+
self.retry_wait = retry_wait
30+
self.check_interval = check_interval
31+
32+
def get_result(self):
33+
error_count = 1
34+
35+
while True:
36+
try:
37+
return self._get_result()
38+
except httpx.HTTPError as e:
39+
if error_count <= self.retry_count:
40+
click.echo(
41+
"Error communicating with ONEKEY platform, retrying; error='{}'".format(
42+
str(e)
43+
)
44+
)
45+
time.sleep(self.retry_wait * error_count)
46+
error_count += 1
47+
else:
48+
click.echo(
49+
"Too many communication error with ONEKEY platform, failing"
50+
)
51+
raise
52+
53+
def _get_result(self):
54+
self.wait_for_analysis_finish()
55+
56+
recent_id = self.get_recent_firmware_id()
57+
if recent_id is not None:
58+
click.echo(
59+
f"Previous firmware results: {self.get_firmware_ui_url(recent_id)}"
60+
)
61+
res = self.client.query(
62+
COMPARE_FIRMWARE, {"base": recent_id, "other": self.firmware_id}
63+
)
64+
new_issues = res["compareFirmwareAnalyses"]["issues"]["new"]
65+
dropped_issues = res["compareFirmwareAnalyses"]["issues"]["dropped"]
66+
new_cves = {
67+
tuple(cve_entry.items())
68+
for cve_entry in res["compareFirmwareAnalyses"]["cveEntries"]["new"]
69+
}
70+
dropped_cves = {
71+
cve_entry["id"]
72+
for cve_entry in res["compareFirmwareAnalyses"]["cveEntries"]["dropped"]
73+
}
74+
else:
75+
click.echo("No previous firmware has been uploaded")
76+
res = self.client.query(LATEST_ISSUES_QUERY, {"id": self.firmware_id})
77+
new_issues = res["firmware"]["latestIssues"]
78+
dropped_issues = []
79+
new_cves = {
80+
tuple(cve_match["cve"].items())
81+
for cve_match in res["firmware"]["cveMatches"]
82+
}
83+
dropped_cves = []
84+
85+
click.echo("#" * 80)
86+
click.echo(
87+
f"New / dropped issue count: {len(new_issues)} / {len(dropped_issues)}"
88+
)
89+
click.echo(f"New / dropped CVE count: {len(new_cves)} / {len(dropped_cves)}")
90+
if recent_id is not None and (
91+
new_issues or dropped_issues or new_cves or dropped_cves
92+
):
93+
click.echo(
94+
f"Firmware comparison results with previous firmware: {self.get_firmware_compare_ui_url(recent_id, self.firmware_id)}"
95+
)
96+
else:
97+
click.echo("No changes since previous firmware")
98+
99+
return new_issues, dropped_issues, new_cves, dropped_cves
100+
101+
def wait_for_analysis_finish(self):
102+
click.echo(f"Waiting for analysis to finish on firmware: {self.firmware_id}")
103+
while True:
104+
try:
105+
self.client.refresh_tenant_token()
106+
107+
res = self.client.query(FIRMWARE_STATUS_QUERY, {"id": self.firmware_id})
108+
if res["firmware"] is None:
109+
click.echo(
110+
"Firmware is not yet available, analysis not started yet, waiting."
111+
)
112+
time.sleep(self.check_interval)
113+
continue
114+
115+
latest_analysis = res["firmware"]["latestAnalysis"]
116+
if latest_analysis is None:
117+
click.echo("Analysis has not started yet, waiting.")
118+
time.sleep(self.check_interval)
119+
continue
120+
121+
if latest_analysis["state"] != "DONE":
122+
click.echo("Firmware analysis still in progress, waiting.")
123+
time.sleep(self.check_interval)
124+
continue
125+
126+
if latest_analysis["result"] != "COMPLETE":
127+
click.echo(
128+
f"Firmware analysis failed, check details: {self.get_firmware_ui_url(self.firmware_id)}"
129+
)
130+
sys.exit(2)
131+
else:
132+
click.echo(
133+
f"Firmware analysis finished successfully, results: {self.get_firmware_ui_url(self.firmware_id)}"
134+
)
135+
break
136+
except Exception as e:
137+
click.echo(f"Error fetching results {str(e)}")
138+
sys.exit(10)
139+
140+
def get_recent_firmware_id(self):
141+
res = self.client.query(GET_ALL_FIRMWARES, {"id": self.firmware_id})
142+
143+
firmware_ids = [
144+
timeline["firmware"]["id"]
145+
for timeline in res["firmware"]["product"]["firmwareTimeline"]
146+
]
147+
148+
latest_id = firmware_ids.pop(0)
149+
if latest_id != self.firmware_id:
150+
click.echo(
151+
f"Latest firmware upload is not the current firmware, skipping comparison with previous, latest={latest_id}"
152+
)
153+
return
154+
155+
if not firmware_ids:
156+
click.echo("No previous firmware")
157+
return
158+
159+
return firmware_ids[0]
160+
161+
def get_firmware_ui_url(self, firmware_id):
162+
return f"https://{self.client.api_url.host}/firmwares?firmwareId={firmware_id}"
163+
164+
def get_firmware_compare_ui_url(self, recent_id, firmware_id):
165+
return f"https://{self.client.api_url.host}/firmwares/compare-firmwares?baseFirmwareId={recent_id}&otherFirmwareId={firmware_id}"
166+
167+
168+
@click.command()
169+
@click.option("--firmware-id", required=True, type=UUID, help="Firmware ID")
170+
@click.option(
171+
"--exit-code-on-new-finding",
172+
"exit_code",
173+
type=int,
174+
default=1,
175+
show_default=True,
176+
help="Exit code to use when findings are identified compared to previous firmware upload",
177+
)
178+
@click.option(
179+
"--check-interval",
180+
type=int,
181+
default=60,
182+
show_default=True,
183+
help="Wait time between checking for result",
184+
)
185+
@click.option(
186+
"--retry-count",
187+
type=int,
188+
default=10,
189+
show_default=True,
190+
help="Number of times to retry fetching results due to communication problem",
191+
)
192+
@click.option(
193+
"--retry-wait",
194+
type=int,
195+
default=60,
196+
show_default=True,
197+
help="Wait time between retries due to communication problem",
198+
)
199+
@click.pass_obj
200+
def ci_result(
201+
client: Client,
202+
firmware_id: UUID,
203+
exit_code: int,
204+
retry_count: int,
205+
retry_wait: int,
206+
check_interval: int,
207+
):
208+
"""Fetch analysis results for CI"""
209+
210+
handler = ResultHandler(
211+
client,
212+
firmware_id,
213+
retry_count=retry_count,
214+
retry_wait=retry_wait,
215+
check_interval=check_interval,
216+
)
217+
new_issues, dropped_issues, new_cves, dropped_cves = handler.get_result()
218+
219+
exit_code = exit_code if new_issues or new_cves else 0
220+
221+
sys.exit(exit_code)

onekey_client/cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from onekey_client import Client
77
from .firmware_upload import upload_firmware
88
from .misc import list_tenants, get_tenant_token
9+
from .ci import ci_result
910

1011

1112
@click.group()
@@ -67,6 +68,7 @@ def cli(ctx, api_url, disable_tls_verify, email, password, tenant_name):
6768
cli.add_command(list_tenants)
6869
cli.add_command(get_tenant_token)
6970
cli.add_command(upload_firmware)
71+
cli.add_command(ci_result)
7072

7173

7274
def main():
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
query CompareFirmware($base: ID!, $other: ID!){
2+
compareFirmwareAnalyses (base: $base, other: $other) {
3+
issues {
4+
new {
5+
__typename
6+
id
7+
severity
8+
type
9+
file {
10+
path
11+
}
12+
}
13+
dropped {
14+
__typename
15+
id
16+
severity
17+
type
18+
file {
19+
path
20+
}
21+
}
22+
}
23+
24+
cveEntries {
25+
new {
26+
id
27+
description
28+
severity
29+
}
30+
dropped {
31+
id
32+
}
33+
}
34+
}
35+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
query GetFirmwareLatestAnalysisState($id: ID!) {
2+
firmware(id: $id) {
3+
name
4+
latestAnalysis {
5+
state
6+
result
7+
}
8+
}
9+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
query GetFimrwareLatestResult($id: ID!){
2+
firmware(id: $id) {
3+
latestIssues (filter: {elf: false}){
4+
__typename
5+
id
6+
severity
7+
type
8+
file {
9+
path
10+
}
11+
}
12+
13+
cveMatches {
14+
component {
15+
name
16+
version
17+
}
18+
cve {
19+
id
20+
description
21+
severity
22+
}
23+
}
24+
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
query GetSameProductFirmwares($id: ID!){
2+
firmware(id: $id) {
3+
product {
4+
firmwareTimeline {
5+
firmware {
6+
id
7+
}
8+
}
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)