Skip to content

Commit d9aa640

Browse files
committed
Introduce fixed paths and import command that signs and uploads
Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent a7d4109 commit d9aa640

2 files changed

Lines changed: 91 additions & 57 deletions

File tree

Tests/cli.py

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,31 @@
66
import os
77
import os.path
88
import re
9+
import shutil
10+
import subprocess
911
import sys
1012
import tempfile
11-
import uuid
1213

1314
import click
1415
import yaml
1516

1617
from scs_cert_lib import load_spec, annotate_validity, add_period, normalize_scope
1718

1819

20+
MONITOR_URL = "https://compliance.sovereignit.cloud/"
1921
HERE = os.path.dirname(__file__)
22+
CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.config', 'scs')
23+
STATE_PATH = os.path.join(os.path.expanduser('~'), '.local', 'share', 'scs')
24+
KEYFILE_PATH = os.path.join(CONFIG_PATH, '.secret', 'keyfile')
25+
TOKENFILE_PATH = os.path.join(CONFIG_PATH, '.secret', 'tokenfile')
2026
DEFAULT_SPECPATH = {
2127
'50393e6f-2ae1-4c5c-a62c-3b75f2abef3f': os.path.join(HERE, 'scs-compatible-iaas.yaml'),
2228
'1fffebe6-fd4b-44d3-a36c-fc58b4bb0180': os.path.join(HERE, 'scs-compatible-kaas.yaml'),
2329
}
30+
SCOPE_DIR = {
31+
'50393e6f-2ae1-4c5c-a62c-3b75f2abef3f': 'scs-compatible-iaas',
32+
'1fffebe6-fd4b-44d3-a36c-fc58b4bb0180': 'scs-compatible-kaas',
33+
}
2434

2535
logger = logging.getLogger(__name__)
2636

@@ -78,14 +88,42 @@ def select(specpath, version, sections, tests):
7888
print('\n'.join(sorted(all_testcase_ids)))
7989

8090

91+
def _sign_report(target_path):
92+
return subprocess.run([
93+
shutil.which('ssh-keygen'),
94+
'-Y', 'sign',
95+
'-f', KEYFILE_PATH,
96+
'-n', 'report',
97+
target_path,
98+
]).returncode
99+
100+
101+
def _upload_report(target_path, monitor_url):
102+
try:
103+
with open(TOKENFILE_PATH, "r") as fileobj:
104+
auth_token = fileobj.read().strip()
105+
except Exception as e:
106+
logger.error(f"Unable to load token ({e!s}). Aborting upload to compliance monitor")
107+
return 1
108+
return subprocess.run([
109+
shutil.which('curl'),
110+
'--fail-with-body',
111+
'--data-binary', f'@{target_path}.sig',
112+
'--data-binary', f'@{target_path}',
113+
'-H', 'Content-Type: application/x-signed-yaml',
114+
'-H', f'Authorization: Basic {auth_token}',
115+
f"{monitor_url.removesuffix('/')}/reports",
116+
]).returncode
117+
118+
81119
def _update_scorecard(scorecard, report, testcase_lookup):
82120
subject = report['subject']
83121
scopeuuid = report['scope']
84-
if subject != scorecard.setdefault('subject', subject):
122+
if subject != scorecard['subject']:
85123
raise RuntimeError('subjects do not match')
86-
if scopeuuid != scorecard.setdefault('scope', scopeuuid):
124+
if scopeuuid != scorecard['scope']:
87125
raise RuntimeError('scopes do not match')
88-
scores = scorecard.setdefault('results', {})
126+
scores = scorecard['results']
89127
uuid = report['uuid']
90128
checked_at = report['checked_at']
91129
checked_at_str = str(checked_at)[:19]
@@ -115,6 +153,7 @@ def _prune_scorecard(scorecard, testcase_lookup, grace_period_days=7):
115153
# keep results for unknown testcases for a week to leave time for migration
116154
if score['checked_at'] > grace_str:
117155
continue
156+
print(tc_id, score['expires_at'], now_str, score['checked_at'], grace_str)
118157
del scores[tc_id]
119158

120159

@@ -132,61 +171,47 @@ def _dump(o):
132171
return yaml.safe_dump(o, default_flow_style=False, sort_keys=False, explicit_start=True)
133172

134173

135-
@cli.command()
136-
@click.option('--subject', '-s', 'subject', type=str)
137-
@click.option('--score', '-S', 'score_yaml', type=click.Path(exists=False), default=None)
138-
@click.option('-o', '--output', 'report_yaml', type=click.Path(exists=False), default=None)
139-
@click.option('--spec', 'specpath', type=click.Path(exists=True))
140-
def score(specpath, subject, score_yaml, report_yaml):
141-
scorecard = {}
142-
if score_yaml and os.path.exists(score_yaml):
143-
with open(score_yaml, "r", encoding="UTF-8") as fileobj:
144-
scorecard = yaml.load(fileobj, Loader=yaml.SafeLoader)
145-
if not subject:
146-
subject = scorecard['subject']
147-
if not specpath:
148-
specpath = scorecard['scope']
149-
elif not subject:
150-
raise click.UsageError('need to supply at least one of -s or -S')
151-
elif not specpath:
152-
raise click.UsageError('need to supply at least one of --spec or -S')
153-
spec = _load_spec(specpath)
154-
scopeuuid = spec['uuid']
155-
snippet = yaml.load(sys.stdin.read(), Loader=yaml.SafeLoader)
156-
if not snippet:
157-
logger.warning('Empty report snippet. Bailing')
174+
@cli.command(name='import')
175+
@click.option('--monitor-url', 'monitor_url', type=str, default=MONITOR_URL)
176+
def import_report(monitor_url):
177+
report = yaml.load(sys.stdin.read(), Loader=yaml.SafeLoader)
178+
if not report:
179+
logger.warning('Empty report. Bailing')
158180
return
159-
report = {
160-
'uuid': str(uuid.uuid4()),
161-
'subject': subject,
162-
'scope': scopeuuid,
163-
**snippet
164-
}
165-
if report_yaml is None:
166-
ts = str(report['checked_at'])[:19]
167-
ts = ts.replace(':', '').replace('-', '').replace(' ', 'T')
168-
report_yaml = f'report-{ts}-{subject}.yaml'
181+
uuid = report['uuid']
182+
subject = report['subject']
183+
scopeuuid = report['scope']
184+
basepath = os.path.join(STATE_PATH, SCOPE_DIR[scopeuuid], subject)
185+
os.makedirs(basepath, exist_ok=True)
186+
# save report
187+
report_yaml = os.path.join(basepath, f'report-{uuid}.yaml')
188+
if os.path.exists(report_yaml):
189+
raise RuntimeError(f"File 'report-{uuid}.yaml' already exists; report uuid NOT UNIQUE?")
169190
_atomic_write(report_yaml, _dump(report))
170-
if score_yaml:
171-
_prune_scorecard(scorecard, spec['testcases'])
172-
_update_scorecard(scorecard, report, spec['testcases'])
173-
_atomic_write(score_yaml, _dump(scorecard))
174-
# collect report uuids
175-
# prune reports
176-
177-
178-
@cli.command()
179-
@click.option('--subject', '-s', 'subject', type=str)
180-
@click.option('--spec', 'specpath', type=click.Path(exists=True))
181-
@click.argument('score_yaml', type=click.Path(exists=False))
182-
def init(specpath, subject, score_yaml):
183-
spec = _load_spec(specpath)
184-
scorecard = {
185-
'subject': subject,
186-
'scope': spec['uuid'],
187-
'results': {},
188-
}
191+
_sign_report(report_yaml)
192+
_upload_report(report_yaml, monitor_url)
193+
# handle scorecard
194+
score_yaml = os.path.join(basepath, 'scorecard.yaml')
195+
if os.path.exists(score_yaml):
196+
with open(score_yaml, "r", encoding="UTF-8") as fileobj:
197+
scorecard = yaml.load(fileobj, Loader=yaml.SafeLoader)
198+
else:
199+
scorecard = {'subject': subject, 'scope': scopeuuid, 'results': {}}
200+
spec = _load_spec(scopeuuid)
201+
_prune_scorecard(scorecard, spec['testcases'])
202+
_update_scorecard(scorecard, report, spec['testcases'])
189203
_atomic_write(score_yaml, _dump(scorecard))
204+
# prune reports that don't occur in scorecard (i.e., old ones)
205+
report_uuids = set(result['report'] for result in scorecard['results'].values())
206+
fns = os.listdir(basepath)
207+
for fn in fns:
208+
if not fn.startswith('report-'):
209+
continue
210+
uuid = fn.removeprefix('report-').removesuffix('.sig').removesuffix('.yaml')
211+
if len(uuid) != 36 or uuid in report_uuids:
212+
continue
213+
logger.info(f"Pruning {fn!s}")
214+
os.unlink(os.path.join(basepath, fn))
190215

191216

192217
if __name__ == "__main__":

Tests/iaas/openstack_test.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import logging
1313
import os
1414
import sys
15+
import uuid
1516

1617
import openstack
1718
import yaml
@@ -256,13 +257,14 @@ def main(argv):
256257
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
257258
openstack.enable_logging(debug=False)
258259
cloud = None
260+
subject = None
259261

260262
try:
261263
cloud = os.environ["OS_CLOUD"]
262264
except KeyError:
263265
pass
264266
try:
265-
opts, args = getopt.gnu_getopt(argv, "c:C:", ("os-cloud=", ))
267+
opts, args = getopt.gnu_getopt(argv, "c:s:", ("os-cloud=", "subject="))
266268
except getopt.GetoptError as exc:
267269
print(f"CRITICAL: {exc!r}", file=sys.stderr)
268270
usage(1)
@@ -271,6 +273,8 @@ def main(argv):
271273
usage(0)
272274
elif opt[0] == "-c" or opt[0] == "--os-cloud":
273275
cloud = opt[1]
276+
elif opt[0] == "-s" or opt[0] == "--subject":
277+
subject = opt[1]
274278
else:
275279
usage(2)
276280

@@ -285,6 +289,8 @@ def main(argv):
285289
if not cloud:
286290
print("CRITICAL: You need to have OS_CLOUD set or pass --os-cloud=CLOUD.", file=sys.stderr)
287291
sys.exit(1)
292+
if not subject:
293+
subject = cloud
288294

289295
c = make_container(cloud)
290296
try:
@@ -301,8 +307,11 @@ def main(argv):
301307
for testcase in testcases:
302308
harness(testcase, results, lambda: getattr(c, testcase.replace('-', '_')))
303309
report = {
310+
'uuid': str(uuid.uuid4()),
304311
'creator': 'openstack_test.py v0.1.0',
305312
'checked_at': datetime.now(),
313+
'subject': subject,
314+
'scope': '50393e6f-2ae1-4c5c-a62c-3b75f2abef3f',
306315
'tests': {
307316
key: {'result': value}
308317
for key, value in results.items()

0 commit comments

Comments
 (0)