Skip to content

Commit 0cda9bc

Browse files
authored
Merge pull request #5 from tinyhumansai/security-complete-7328
Complete native security engines and scoped policy enforcement
2 parents b6394ad + 03d5e36 commit 0cda9bc

145 files changed

Lines changed: 26786 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
"""Enforce native file and changed-line coverage without dependencies.
3+
4+
JSON supplies per-file executable counts; LCOV supplies changed executable lines.
5+
Only exact checkout-relative crate paths are admitted. Both Windows audit sources
6+
must be instrumented on Windows; Unix runners require the native journal and sink.
7+
"""
8+
import argparse
9+
import json
10+
import os
11+
import re
12+
import subprocess
13+
import sys
14+
from pathlib import Path
15+
16+
REQUIRED = (
17+
'crates/tinysecurity-audit/src/windows.rs',
18+
'crates/tinysecurity-audit/src/windows_acl.rs',
19+
)
20+
21+
UNIX_REQUIRED = (
22+
'crates/tinysecurity-audit/src/sink.rs',
23+
'crates/tinysecurity-audit/src/journal.rs',
24+
'crates/tinysecurity-audit/src/rotation.rs',
25+
)
26+
27+
28+
def required_sources(platform):
29+
"""Select actual native sources, never require disabled Windows code on Unix."""
30+
return REQUIRED if platform == 'windows' else UNIX_REQUIRED
31+
32+
33+
def relative_source(filename, root):
34+
"""Return a crate path under the exact checkout, or None for other files."""
35+
filename = filename.replace('\\', '/')
36+
root = root.replace('\\', '/').rstrip('/')
37+
# Windows drive paths are case insensitive; POSIX paths are not.
38+
windows = bool(re.match(r'^[A-Za-z]:/', root))
39+
candidate = filename.casefold() if windows else filename
40+
prefix = (root.casefold() if windows else root) + '/'
41+
if not candidate.startswith(prefix):
42+
return None
43+
relative = filename[len(prefix):]
44+
if windows:
45+
relative = relative.casefold()
46+
parts = relative.split('/')
47+
if '\x00' in relative or any(part in ('', '.', '..') for part in parts):
48+
raise ValueError(f'invalid checkout source path: {filename}')
49+
if relative.startswith('vendor/'):
50+
return None
51+
if not relative.startswith('crates/') or not relative.endswith('.rs'):
52+
raise ValueError(f'unknown checkout source path: {filename}')
53+
return relative
54+
55+
56+
def check_files(report, root, minimum, platform="windows"):
57+
"""Reject absent, empty, inconsistent, or below-threshold source reports."""
58+
files = {}
59+
for datum in report['data']:
60+
for entry in datum['files']:
61+
name = relative_source(entry['filename'], root)
62+
if name is None:
63+
continue
64+
lines = entry['summary']['lines']
65+
count, covered = lines['count'], lines['covered']
66+
if (type(count) is not int or type(covered) is not int
67+
or count < 0 or covered < 0 or covered > count):
68+
raise ValueError(f'invalid executable counts: {name}')
69+
counts = (covered, count)
70+
if name in files and files[name] != counts:
71+
raise ValueError(f'conflicting duplicate source: {name}')
72+
files[name] = counts
73+
for name in required_sources(platform):
74+
if name not in files:
75+
raise ValueError(f'missing required {platform.title()} source: {name}')
76+
if files[name][1] == 0:
77+
raise ValueError(f'no executable lines in required {platform.title()} source: {name}')
78+
for name, (covered, count) in sorted(files.items()):
79+
if count and covered * 100 < minimum * count:
80+
raise ValueError(f'{name}: {covered}/{count} lines below {minimum}%')
81+
return files
82+
83+
84+
def parse_lcov(report, root):
85+
"""Normalize LCOV SF records and merge repeated instrumentation counts."""
86+
files = {}
87+
current = None
88+
for record in report.splitlines():
89+
if record.startswith('SF:'):
90+
current = relative_source(record[3:], root)
91+
if current is not None:
92+
files.setdefault(current, {})
93+
elif record.startswith('DA:') and current is not None:
94+
fields = record[3:].split(',')
95+
if len(fields) not in (2, 3):
96+
raise ValueError(f'invalid LCOV line record: {current}')
97+
line, count = int(fields[0]), int(fields[1])
98+
if line <= 0 or count < 0:
99+
raise ValueError(f'invalid LCOV line: {current}')
100+
files[current][line] = files[current].get(line, 0) + count
101+
elif record == 'end_of_record':
102+
current = None
103+
return files
104+
105+
106+
def production_source(name):
107+
"""Tests are not production changes; their JSON file coverage still applies."""
108+
return (name.startswith('crates/') and '/src/' in name
109+
and name.endswith('.rs') and not name.endswith('_tests.rs'))
110+
111+
112+
def changed_lines(diff):
113+
"""Extract added/modified production line ranges from git's unified=0 diff."""
114+
files = {}
115+
current = None
116+
for line in diff.splitlines():
117+
if line.startswith('+++ '):
118+
name = line[4:]
119+
if not name.startswith('b/') and name != '/dev/null':
120+
raise ValueError('unrecognized git diff source path')
121+
current = name[2:] if name.startswith('b/') else None
122+
if current is not None and any(part in ('', '.', '..') for part in current.split('/')):
123+
raise ValueError('invalid git diff source path')
124+
if current is not None and not production_source(current):
125+
current = None
126+
elif current is not None and line.startswith('@@ '):
127+
match = re.match(r'@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line)
128+
if match is None:
129+
raise ValueError('invalid git diff hunk')
130+
first = int(match[1])
131+
count = int(match[2]) if match[2] is not None else 1
132+
if count:
133+
files.setdefault(current, set()).update(range(first, first + count))
134+
return files
135+
136+
137+
def check_changed(lines, changed, minimum, compiled, platform="windows"):
138+
"""Cross-check compiled production admission before measuring changed lines.
139+
140+
JSON is the native compilation inventory: platform-disabled sources are
141+
absent, and declaration-only sources have zero executable lines. Neither
142+
requires LCOV. Every admitted executable production source does, even when
143+
unchanged, so missing instrumentation cannot silently shrink the gate.
144+
"""
145+
for name in required_sources(platform):
146+
if not lines.get(name):
147+
raise ValueError(f'no measured changed or executable LCOV lines: {name}')
148+
for name, (_, count) in compiled.items():
149+
if production_source(name) and count and not lines.get(name):
150+
raise ValueError(f'missing executable LCOV source: {name}')
151+
for name, executable in lines.items():
152+
if production_source(name) and executable and (name not in compiled or not compiled[name][1]):
153+
raise ValueError(f'LCOV source absent from executable JSON inventory: {name}')
154+
# Sources disabled on this platform and declaration-only files supply no
155+
# executable denominator. Inventory consistency above still runs first.
156+
changed = {name: changed_set for name, changed_set in changed.items()
157+
if name in compiled and compiled[name][1]}
158+
if not changed:
159+
return None
160+
for name in required_sources(platform):
161+
if name not in changed:
162+
continue
163+
measured = set(lines.get(name, {})) & changed.get(name, set())
164+
if not measured:
165+
raise ValueError(f'no measured changed executable lines: {name}')
166+
covered = total = 0
167+
for name, changed_set in changed.items():
168+
for line in changed_set & set(lines.get(name, {})):
169+
total += 1
170+
covered += lines[name][line] > 0
171+
if not total:
172+
raise ValueError('no measured changed executable lines in workspace')
173+
if covered * 100 < minimum * total:
174+
raise ValueError(f'changed lines: {covered}/{total} below {minimum}%')
175+
return covered, total
176+
177+
178+
def main():
179+
parser = argparse.ArgumentParser(description=__doc__)
180+
parser.add_argument('--json', required=True)
181+
parser.add_argument('--lcov', required=True)
182+
parser.add_argument('--base', required=True, help='canonical PR base commit/ref')
183+
parser.add_argument('--root', default=str(Path.cwd()))
184+
parser.add_argument('--platform', choices=('windows', 'linux', 'macos'), default='windows')
185+
args = parser.parse_args()
186+
files = check_files(json.loads(Path(args.json).read_text(encoding='utf-8')), args.root, 90, args.platform)
187+
print('File\tLine coverage\tCovered lines\tCoverable lines')
188+
for name, (covered, count) in sorted(files.items()):
189+
if count:
190+
print(f'{name}\t{covered * 100 / count:.2f}%\t{covered}\t{count}')
191+
# Decode after byte capture: Windows text-reader threads can lose stdout
192+
# on a decoding failure. Strict UTF-8 errors must reject the gate instead.
193+
diff = subprocess.run(
194+
['git', '-c', 'core.quotePath=false', 'diff', '--no-ext-diff', '--no-renames',
195+
'--unified=0', args.base, 'HEAD', '--', 'crates/'],
196+
cwd=args.root, check=True, capture_output=True,
197+
).stdout.decode('utf-8')
198+
changed = changed_lines(diff)
199+
lcov = parse_lcov(Path(args.lcov).read_text(encoding='utf-8'), args.root)
200+
result = check_changed(lcov, changed, 80, files, args.platform)
201+
if result is None:
202+
print('No changed compiled executable sources relative to canonical base; file gate passed.')
203+
return
204+
covered, total = result
205+
print(f'Changed executable lines: {covered}/{total} ({covered * 100 / total:.2f}%)')
206+
if os.environ.get('GITHUB_STEP_SUMMARY'):
207+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary:
208+
summary.write(f'Native {args.platform} coverage: {len(files)} source reports; '
209+
f'changed executable lines {covered}/{total}.\n')
210+
211+
212+
if __name__ == '__main__':
213+
try:
214+
main()
215+
except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error:
216+
print(f'native coverage rejected: {error}', file=sys.stderr)
217+
sys.exit(1)

0 commit comments

Comments
 (0)