Merge ~ebarretto/ubuntu-cve-tracker:oval-lsn into ubuntu-cve-tracker:master
- Git
- lp:~ebarretto/ubuntu-cve-tracker
- oval-lsn
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 3cc230c34f98e9fe6f4128380215a95f6d416928 |
Proposed branch: | ~ebarretto/ubuntu-cve-tracker:oval-lsn |
Merge into: | ubuntu-cve-tracker:master |
Diff against target: |
411 lines (+178/-30) 4 files modified
scripts/fetch-lsns.py (+81/-0) scripts/generate-oval (+14/-12) scripts/oval_lib.py (+78/-16) test/test_oval_lib_unit.py (+5/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Seth Arnold | Approve | ||
Alex Murray | Approve | ||
Review via email: mp+429162@code.launchpad.net |
Commit message
Description of the change
This PR adds LSNs to USN OVAL generation, as well as a fetch-lsns script.
Therefore an USN OVAL will contain both USNs and LSNs. The LSNs will be on their own definition, and basically it will check if you canonical-livepatch on your system, and if yes, it will check if you have the module version from the LSN loaded in /proc/modules. That way avoiding false positives if you don't have livepatch on your system.
This doesn't affect OCI USN OVAL, which relies on a manifest file and doesn't scan the actual system.
Currently this change is breaking 3 tests under test/ and that's because I changed the reference URL to USNs and LSNs. We had a hardcoded 'USN-' in the URL. To fix it I can either:
1. Change our infrastructure and USN DB to have keys as 'USN-XXXX-Y' instead of just 'XXXX-Y'
2. Do some logic before generating the URL
The 1 change requires more effort and will implicate in changes to our USN DB.
PS: We decided to go with 2. This PR is now ready for review + merge.
Seth Arnold (seth-arnold) wrote : | # |
Eduardo Barretto (ebarretto) wrote : | # |
Thanks Seth!
I added a small hack to make the id as USN-XXXX-YY until we address anything in the USN DB itself.
> (Grabbing only twenty LSNs at a time, from the web server, seems likely to be brittle some day. We may not be able to do any better if this is what the web team has given us.)
The idea is to get 20, as this is the default from the search page, more than it makes the request to take a longer time to get an answer, if any at all, as sometimes it just times out. And in the request I make, it is in newest order, which make us only get the latest one and it stops requesting whenever it hits an LSN that is already in the JSON file.
Eduardo Barretto (ebarretto) wrote : | # |
It has been 10 days since last update.
Could someone review this?
Seth Arnold (seth-arnold) wrote : | # |
Sorry :( my only thought on the newer changes is that it might be nice to put the hack in a function, so when (if?) we ever get to undo it, we only have to do it in one place.
It's only two lines though, maybe it's not a big deal.
Thanks
Alex Murray (alexmurray) wrote : | # |
LGTM - minor comment re adding the USN- prefix to USN IDs.
Eduardo Barretto (ebarretto) wrote : | # |
Thanks Alex and Seth,
I took both suggestions into consideration and added a prepend_usn_to_id function using regex, making it easier to adjust or remove it whenever we need.
I've also gave +x to the fetch-lsns script.
I would appreciate a check on both last commits just to make sure I didn't break anything.
Thanks!
Seth Arnold (seth-arnold) wrote : | # |
a988b5f and a495a0c look good to me! I do kinda vaguely wonder how much time python's going to spend compiling that regex over and over again but lets just pretend it's fine for now and get this landed.
Thanks :)
Preview Diff
1 | diff --git a/scripts/fetch-lsns.py b/scripts/fetch-lsns.py |
2 | new file mode 100755 |
3 | index 0000000..109a149 |
4 | --- /dev/null |
5 | +++ b/scripts/fetch-lsns.py |
6 | @@ -0,0 +1,81 @@ |
7 | +#!/usr/bin/env python3 |
8 | +# |
9 | +# Author: Eduardo Barretto |
10 | +# Copyright (C) 2022- Canonical Ltd. |
11 | +# |
12 | +# This script is distributed under the terms and conditions of the GNU General |
13 | +# Public License, Version 3 or later. See http://www.gnu.org/copyleft/gpl.html |
14 | +# for details. |
15 | + |
16 | +import datetime |
17 | +import json |
18 | +import requests |
19 | +import os |
20 | + |
21 | +url = "https://ubuntu.com/security/notices.json?order=newest&details=LSN" |
22 | +data = json.loads(requests.get(url).text) |
23 | +total = data['total_results'] |
24 | +filename = 'database-lsn.json' |
25 | + |
26 | +db = {} |
27 | +if os.path.exists(filename): |
28 | + with open(filename, 'r') as json_file: |
29 | + try: |
30 | + db = json.load(json_file) |
31 | + print('Reading database-lsn.json') |
32 | + except json.decoder.JSONDecodeError: |
33 | + print('Creating database-lsn.json') |
34 | + |
35 | +offset = 0 |
36 | +with open(filename, 'w+') as json_file: |
37 | + while offset < total: |
38 | + data = json.loads(requests.get(url + '&offset=' + str(offset)).text) |
39 | + for notice in data['notices']: |
40 | + lsn_id = notice['id'] |
41 | + if lsn_id in db: |
42 | + print('database is up-to-date') |
43 | + offset = total |
44 | + break |
45 | + else: |
46 | + print('importing {}'.format(lsn_id)) |
47 | + db[lsn_id] = {} |
48 | + db[lsn_id]['description'] = notice['description'] |
49 | + db[lsn_id]['releases'] = {} |
50 | + for release in notice['release_packages']: |
51 | + db[lsn_id]['releases'][release] = {'sources': {}, |
52 | + 'binaries': {}, |
53 | + 'allbinaries': {}} |
54 | + for item in notice['release_packages'][release]: |
55 | + # lsn json have two entries for same source |
56 | + # one containing the binary as version and |
57 | + # another with the livepatch module version |
58 | + if not item['is_source']: |
59 | + continue |
60 | + db[lsn_id]['releases'][release]['sources'][item['name']] = { |
61 | + 'version': item['version'], |
62 | + 'description': item['description'] |
63 | + } |
64 | + version = item['version'].replace('.', '_') |
65 | + module_name = "lkp_Ubuntu_" + version.split('-')[0] + \ |
66 | + r"[_|\d]+_" + item['name'].split('-')[0] + "_(\d+)" |
67 | + db[lsn_id]['releases'][release]['allbinaries'][item['name']] = { |
68 | + "pocket": "livepatch", |
69 | + "module": module_name, |
70 | + "version": lsn_id.split('-')[1].lstrip('0') |
71 | + } |
72 | + db[lsn_id]['title'] = notice['title'] |
73 | + date = datetime.datetime.strptime(notice['published'], "%Y-%m-%dT%H:%M:%S") |
74 | + db[lsn_id]['timestamp'] = datetime.datetime.timestamp(date) |
75 | + db[lsn_id]['summary'] = notice['title'] |
76 | + db[lsn_id]['action'] = notice['instructions'] |
77 | + db[lsn_id]['is_hidden'] = 'False' |
78 | + db[lsn_id]['cves'] = notice['cves_ids'] |
79 | + db[lsn_id]['id'] = notice['id'] |
80 | + db[lsn_id]['isummary'] = notice['summary'] |
81 | + db[lsn_id]['related_notices'] = [] |
82 | + for rn in notice['related_notices']: |
83 | + db[lsn_id]['related_notices'].append(rn['id']) |
84 | + |
85 | + offset += 20 |
86 | + |
87 | + json.dump(db, json_file, indent=4) |
88 | diff --git a/scripts/generate-oval b/scripts/generate-oval |
89 | index 0b91d9a..ddcd74b 100755 |
90 | --- a/scripts/generate-oval |
91 | +++ b/scripts/generate-oval |
92 | @@ -466,7 +466,6 @@ def debug(message): |
93 | if debug_level > 0: |
94 | sys.stdout.write('\rDEBUG: {0}\n'.format(message)) |
95 | |
96 | - |
97 | def progress_bar(current, total, size=20): |
98 | """ show a simple progress bar on the CLI """ |
99 | current_percent = float(current) / total |
100 | @@ -478,6 +477,10 @@ def progress_bar(current, total, size=20): |
101 | |
102 | sys.stdout.flush() |
103 | |
104 | +def prepend_usn_to_id(usn_database, usn_id): |
105 | + if re.search(r'^[0-9]+-[0-9]$', usn_id): |
106 | + usn_database[usn_id]['id'] = 'USN-' + usn_id |
107 | + |
108 | |
109 | # Class to contain the binary package cache |
110 | class PackageCache(): |
111 | @@ -634,15 +637,12 @@ class PackageCache(): |
112 | # loads usn database.json based given a path to it. |
113 | # To get the database proceed as: $UCT/scripts/fetch-db database.json.bz2 |
114 | def get_usn_database(usn_db_dir): |
115 | - data = None |
116 | - default_usn_database = os.path.join(usn_db_dir, 'database.json') |
117 | - if not os.path.exists(default_usn_database): |
118 | - error('{} must exists'.format(default_usn_database)) |
119 | + data = {} |
120 | + for filename in glob.glob(os.path.join(usn_db_dir, 'database*.json')): |
121 | + with open(filename, 'r') as f: |
122 | + data.update(json.load(f)) |
123 | |
124 | - with open(default_usn_database, 'r') as database: |
125 | - data = json.load(database) |
126 | - return data |
127 | - return None |
128 | + return data |
129 | |
130 | # Usage: |
131 | # for a given release only: |
132 | @@ -693,18 +693,20 @@ def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=N |
133 | |
134 | # Generate OVAL USN data |
135 | if usn: |
136 | + prepend_usn_to_id(usn_database, usn) |
137 | for oval in ovals: |
138 | - oval.generate_usn_oval(usn_database[usn], usn, cve_dir) |
139 | + oval.generate_usn_oval(usn_database[usn], usn_database[usn]['id'], cve_dir) |
140 | else: |
141 | for usn in usn_database.keys(): |
142 | + prepend_usn_to_id(usn_database, usn) |
143 | for oval in ovals: |
144 | - oval.generate_usn_oval(usn_database[usn], usn, cve_dir) |
145 | + oval.generate_usn_oval(usn_database[usn], usn_database[usn]['id'], cve_dir) |
146 | |
147 | for oval in ovals: |
148 | oval.write_oval_elements() |
149 | |
150 | - |
151 | return True |
152 | |
153 | + |
154 | if __name__ == '__main__': |
155 | main() |
156 | diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py |
157 | index 95c3856..2d86175 100644 |
158 | --- a/scripts/oval_lib.py |
159 | +++ b/scripts/oval_lib.py |
160 | @@ -53,6 +53,12 @@ def _open(fn, mode, encoding='utf-8'): |
161 | return fd |
162 | |
163 | def prepare_instructions(instruction, cve, product_description, package): |
164 | + if "LSN" in cve: |
165 | + instruction = """\n |
166 | +To check your kernel type and Livepatch version, enter this command: |
167 | + |
168 | +canonical-livepatch status""" |
169 | + |
170 | if not instruction: |
171 | instruction = """\n |
172 | Update Instructions: |
173 | @@ -64,7 +70,9 @@ by updating your system to the following package versions:""".format(cve) |
174 | for binary in package["binaries"]: |
175 | instruction += """{0} - {1}\n""".format(binary, package["fix-version"]) |
176 | |
177 | - if "Long Term" in product_description or "Interim" in product_description: |
178 | + if "LSN" in cve: |
179 | + instruction += "Livepatch subscription required" |
180 | + elif "Long Term" in product_description or "Interim" in product_description: |
181 | instruction += "No subscription required" |
182 | else: |
183 | instruction += product_description |
184 | @@ -737,7 +745,7 @@ class OvalGeneratorUSN(): |
185 | 'variable') |
186 | cve_base_url = 'https://ubuntu.com/security/{}' |
187 | mitre_base_url = 'https://cve.mitre.org/cgi-bin/cvename.cgi?name={}' |
188 | - usn_base_url = 'https://ubuntu.com/security/notices/USN-{}' |
189 | + usn_base_url = 'https://ubuntu.com/security/notices/{}' |
190 | lookup_cve_path = ['./active', './retired'] |
191 | generator_version = '1' |
192 | oval_schema_version = '5.11.1' |
193 | @@ -746,6 +754,7 @@ class OvalGeneratorUSN(): |
194 | def __init__(self, release_codename, release_name, outdir='./', cve_dir=None, prefix='', oval_format='dpkg'): |
195 | self.release_codename = release_codename.replace('/', '_') |
196 | self.release_name = release_name |
197 | + self.pocket = "security" |
198 | self.product_description = None |
199 | self.current_oval = None |
200 | self.tmpdir = tempfile.mkdtemp(prefix='oval_lib-') |
201 | @@ -940,7 +949,7 @@ class OvalGeneratorUSN(): |
202 | 'usn_id': usn_object['id'], |
203 | 'ns': self.ns, |
204 | 'title': "{} -- {}".format(usn_object['id'], usn_object['title']), |
205 | - 'plataform': "{}".format(self.release_name), |
206 | + 'platform': "{}".format(self.release_name), |
207 | 'usn_url': self.usn_base_url.format(usn_object['id']), |
208 | 'description': escape(' '.join((usn_object['description'].strip() + instructions).split('\n'))), |
209 | 'cves_references': cve_references, |
210 | @@ -954,7 +963,12 @@ class OvalGeneratorUSN(): |
211 | criteria = [] |
212 | kernel = False |
213 | for test_ref in test_refs: |
214 | - if 'kernel' in test_ref and self.oval_format == 'dpkg': |
215 | + if self.pocket == 'livepatch' and self.oval_format == 'dpkg': |
216 | + criteria.append('<criteria operator="AND">') |
217 | + criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, str(int(test_ref['testref_id']) + 1), self.product_description)) |
218 | + criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, test_ref['testref_id'], self.product_description)) |
219 | + criteria.append('</criteria>') |
220 | + elif 'kernel' in test_ref and self.oval_format == 'dpkg': |
221 | kernel = True |
222 | criteria.append('<criteria operator="AND">') |
223 | criteria.append(' <criterion test_ref="{0}:tst:{1}" comment="{2}" />'.format(self.ns, test_ref['testref_id'], self.product_description)) |
224 | @@ -972,9 +986,9 @@ class OvalGeneratorUSN(): |
225 | <metadata> |
226 | <title>{title}</title> |
227 | <affected family="unix"> |
228 | - <platform>{plataform}</platform> |
229 | + <platform>{platform}</platform> |
230 | </affected> |
231 | - <reference source="USN" ref_url="{usn_url}" ref_id="USN-{usn_id}"/> |
232 | + <reference source="USN" ref_url="{usn_url}" ref_id="{usn_id}"/> |
233 | {cves_references} |
234 | <description>{description}</description> |
235 | <advisory from="security@ubuntu.com"> |
236 | @@ -1015,6 +1029,19 @@ class OvalGeneratorUSN(): |
237 | <ind:state state_ref="{ns}:ste:{id}"/> |
238 | </ind:variable_test>""".format(**mapping) |
239 | |
240 | + elif self.pocket == 'livepatch': |
241 | + mapping['liv-id'] = str(int(test_ref['testref_id']) + 1) |
242 | + test = \ |
243 | + """ |
244 | + <unix:file_test id="{ns}:tst:{liv-id}" version="1" check="all" check_existence="all_exist" comment="canonical-livepatch installed"> |
245 | + <unix:object object_ref="{ns}:obj:{liv-id}" /> |
246 | + <unix:state state_ref="{ns}:ste:{liv-id}" /> |
247 | + </unix:file_test> |
248 | + <ind:textfilecontent54_test id="{ns}:tst:{id}" version="1" check="all" check_existence="all_exist" comment="livepatch testing"> |
249 | + <ind:object object_ref="{ns}:obj:{id}"/> |
250 | + <ind:state state_ref="{ns}:ste:{id}"/> |
251 | + </ind:textfilecontent54_test>""".format(**mapping) |
252 | + |
253 | else: |
254 | test = \ |
255 | """ |
256 | @@ -1053,6 +1080,21 @@ class OvalGeneratorUSN(): |
257 | <ind:var_ref>{ns}:var:{id}</ind:var_ref> |
258 | </ind:variable_object>""".format(**mapping) |
259 | |
260 | + elif self.pocket == "livepatch": |
261 | + mapping['liv-id'] = str(int(test_ref['testref_id']) + 1) |
262 | + mapping['module'] = test_ref['pkgs'] |
263 | + _object = \ |
264 | + """ |
265 | + <unix:file_object id="{ns}:obj:{liv-id}" version="1" comment="{product}"> |
266 | + <unix:filepath>/snap/bin/canonical-livepatch</unix:filepath> |
267 | + </unix:file_object> |
268 | + <ind:textfilecontent54_object id="{ns}:obj:{id}" version="1" comment="{product}"> |
269 | + <ind:filepath datatype="string">/proc/modules</ind:filepath> |
270 | + <!-- <ind:pattern operation="pattern match">^{module}\s.*$</ind:pattern> --> |
271 | + <ind:pattern operation="pattern match" var_ref="{ns}:var:{id}" var_check="at least one" /> |
272 | + <ind:instance datatype="int">1</ind:instance> |
273 | + </ind:textfilecontent54_object>""".format(**mapping) |
274 | + |
275 | else: |
276 | _object = \ |
277 | """ |
278 | @@ -1100,6 +1142,18 @@ class OvalGeneratorUSN(): |
279 | <ind:value operation="greater than" datatype="debian_evr_string" var_ref="{ns}:var:{varid}" var_check="at least one" /> |
280 | </ind:variable_state>""".format(**mapping) |
281 | |
282 | + elif self.pocket == "livepatch": |
283 | + mapping['liv-id'] = str(int(test_ref['testref_id']) + 1) |
284 | + mapping['bversion'] = binary_version |
285 | + state = \ |
286 | + """ |
287 | + <unix:file_state id="{ns}:ste:{liv-id}" version="1"> |
288 | + <unix:size datatype="int" operation="greater than">0</unix:size> |
289 | + </unix:file_state> |
290 | + <ind:textfilecontent54_state id="{ns}:ste:{id}" version="1"> |
291 | + <ind:subexpression datatype="int" operation="less than">{bversion}</ind:subexpression> |
292 | + </ind:textfilecontent54_state>""".format(**mapping) |
293 | + |
294 | else: |
295 | if binary_version.find(':') != -1: |
296 | mapping['bversion'] = binary_version |
297 | @@ -1250,7 +1304,12 @@ class OvalGeneratorUSN(): |
298 | def get_version_from_binaries(self, usn_allbinaries): |
299 | version_map = collections.defaultdict(list) |
300 | for k, v in usn_allbinaries.items(): |
301 | - version_map[v['version']].append(k) |
302 | + if 'module' in v: |
303 | + self.pocket = 'livepatch' |
304 | + version_map[v['version']].append(v['module']) |
305 | + else: |
306 | + self.pocket = 'security' |
307 | + version_map[v['version']].append(k) |
308 | |
309 | return version_map |
310 | |
311 | @@ -1289,20 +1348,19 @@ class OvalGeneratorUSN(): |
312 | return usn_allbinaries |
313 | |
314 | def update_release_name_from_pocket_or_stamp(self, binaries, stamp): |
315 | - pocket = "security" |
316 | for b in binaries: |
317 | try: |
318 | - pocket = binaries[b]['pocket'] |
319 | + self.pocket = binaries[b]['pocket'] |
320 | break |
321 | except KeyError: |
322 | # trusty usns don't have pocket, so try to check on timestamp |
323 | if self.release_codename == 'trusty' and stamp >= release_stamp('esm/trusty'): |
324 | - pocket = 'esm' |
325 | + self.pocket = 'esm' |
326 | else: |
327 | - pocket = 'security' |
328 | + self.pocket = 'security' |
329 | break |
330 | |
331 | - if pocket in ['security', 'updates']: |
332 | + if self.pocket in ['security', 'updates', 'livepatch']: |
333 | self.release_name = release_name(self.release_codename) |
334 | self.product_description = get_subproject_description(self.release_codename) |
335 | else: |
336 | @@ -1311,8 +1369,8 @@ class OvalGeneratorUSN(): |
337 | self.release_name = release_name('esm/' + self.release_codename) |
338 | self.product_description = get_subproject_description('esm/' + self.release_codename) |
339 | else: |
340 | - self.release_name = release_name(pocket + '/' + self.release_codename) |
341 | - self.product_description = get_subproject_description(pocket + '/' + self.release_codename) |
342 | + self.release_name = release_name(self.pocket + '/' + self.release_codename) |
343 | + self.product_description = get_subproject_description(self.pocket + '/' + self.release_codename) |
344 | |
345 | def generate_usn_oval(self, usn_object, usn_number, cve_dir): |
346 | if self.release_codename not in usn_object['releases'].keys(): |
347 | @@ -1327,6 +1385,11 @@ class OvalGeneratorUSN(): |
348 | |
349 | binary_versions = self.get_version_from_binaries(usn_allbinaries) |
350 | |
351 | + # OCI OVAL does not check running system, therefore it can |
352 | + # skip LSNs |
353 | + if self.oval_format == "oci" and self.pocket == 'livepatch': |
354 | + return |
355 | + |
356 | # group binaries with same version (most likely from same source) |
357 | # and create a test_ref for the group to be used when creating |
358 | # the oval def, test, state and var. |
359 | @@ -1343,12 +1406,11 @@ class OvalGeneratorUSN(): |
360 | # prepare update instructions |
361 | pkg['binaries'] = binary_versions[key] |
362 | pkg['fix-version'] = key |
363 | - instructions = prepare_instructions(instructions, "USN-" + usn_number, self.product_description, pkg) |
364 | + instructions = prepare_instructions(instructions, usn_object['id'], self.product_description, pkg) |
365 | |
366 | # Create the oval objects |
367 | # Only need one definition, but if multiple versions of binary pkgs, |
368 | # then may need several test, object, state and var |
369 | - |
370 | usn_def = self.create_usn_definition(usn_object, usn_number, id_base, test_refs, cve_dir, instructions) |
371 | self.oval_structure['definition'].write(usn_def) |
372 | |
373 | diff --git a/test/test_oval_lib_unit.py b/test/test_oval_lib_unit.py |
374 | index c99a41b..f603210 100644 |
375 | --- a/test/test_oval_lib_unit.py |
376 | +++ b/test/test_oval_lib_unit.py |
377 | @@ -52,6 +52,8 @@ class TestOvalLibUnit: |
378 | usn_mock = "4388-1" |
379 | id_base_mock = 43881000000 |
380 | test_cve_file = "CVE-TEST" |
381 | + usn_object_mock['id'] = "USN-" + usn_mock |
382 | + |
383 | |
384 | bin_dict_mock = collections.defaultdict(list) |
385 | bin_dict_mock = {'5.0.0.1042.27': ['linux-image-gke-5.0'], '5.0.0-1059.64': |
386 | @@ -83,7 +85,7 @@ class TestOvalLibUnit: |
387 | definition_mock = """ |
388 | <definition id="oval:com.ubuntu.bionic:def:43881000000" version="1" class="patch"> |
389 | <metadata> |
390 | - <title>4388-1 -- Linux kernel vulnerabilities</title> |
391 | + <title>USN-4388-1 -- Linux kernel vulnerabilities</title> |
392 | <affected family="unix"> |
393 | <platform>Ubuntu 18.04 LTS</platform> |
394 | </affected> |
395 | @@ -276,7 +278,7 @@ class TestOvalLibUnit: |
396 | invalid_priority_ret = """ |
397 | <definition id="oval:com.ubuntu.bionic:def:43881000000" version="1" class="patch"> |
398 | <metadata> |
399 | - <title>4388-1 -- Linux kernel vulnerabilities</title> |
400 | + <title>USN-4388-1 -- Linux kernel vulnerabilities</title> |
401 | <affected family="unix"> |
402 | <platform>Ubuntu 18.04 LTS</platform> |
403 | </affected> |
404 | @@ -497,6 +499,7 @@ No subscription required""" |
405 | create_bug_ref_mock.return_value = self.url_ref_mock |
406 | get_usn_severity_mock.return_value = self.avg_severity_mock |
407 | |
408 | + print(self.usn_object_mock) |
409 | definition_ret = oval_lib.OvalGeneratorUSN.create_usn_definition( |
410 | self.oval_gen_mock, self.usn_object_mock, self.usn_mock, |
411 | self.id_base_mock, self.test_refs_mock, rel_test_path, |
Changing the USN DB from XXXX-Y to USN-XXXX-Y sounds really likely to cause problems in places we may not even know exist; can we add the LSNs with keys LSN-XXXX-Y and keep the USNs with XXXX-Y, and simply tolerate the asymmetry?
I know that adds more gross stuff here, but saves us from learning what needs to change in eg landscape and whoever else might be consuming the database.
I didn't spot anything in the code, but this is all pretty foreign to me. (Grabbing only twenty LSNs at a time, from the web server, seems likely to be brittle some day. We may not be able to do any better if this is what the web team has given us.)
Thanks