66import os
77import os .path
88import re
9+ import shutil
10+ import subprocess
911import sys
1012import tempfile
11- import uuid
1213
1314import click
1415import yaml
1516
1617from scs_cert_lib import load_spec , annotate_validity , add_period , normalize_scope
1718
1819
20+ MONITOR_URL = "https://compliance.sovereignit.cloud/"
1921HERE = 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' )
2026DEFAULT_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
2535logger = 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+
81119def _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
192217if __name__ == "__main__" :
0 commit comments