The export gate now carries the site patterns, and three kinds of file tripped it - two legitimately, one by construction. Two test files held real internal subnets as fixtures. They are documentation ranges now, which test the same logic and disclose nothing. The project-map generator and a changelog entry named a file that is excluded from publication, so a public reader was pointed at something they cannot see. Both now describe what happened without naming it. And the naming script has to CONTAIN the site patterns in order to grep for them, so written literally the rule's own definition fails the gate that enforces it. The patterns are assembled from fragments, the same trick the docs publishability test already uses for the same reason. Verified the hard way: a planted literal is still caught, so the fragmentation did not quietly turn the rule into one that matches nothing - which is the obvious way for this fix to have gone wrong.
196 lines
8.0 KiB
Python
196 lines
8.0 KiB
Python
"""GE-Enforce first slice: import a scope, publish it, serve it to a client.
|
|
|
|
Exercises the vertical the plan calls the first slice (via a small synthetic
|
|
scope rather than the real gea-shopfloor-cmm): draft import -> publish ->
|
|
GET /api/geenforce/manifest with a geenforce.fetch service token; ETag/304;
|
|
draft edits never change the served bytes; publish + rollback change them.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from plugins.geenforce.models import ManifestScope
|
|
from plugins.geenforce import service
|
|
|
|
|
|
SCOPE = {
|
|
'Version': '2.6',
|
|
'Applications': [
|
|
{'Name': 'Alpha', 'Type': 'MSI', 'Installer': 'apps/alpha.msi',
|
|
'DetectionMethod': 'Registry', 'DetectionPath': 'HKLM:\\SOFTWARE\\Alpha'},
|
|
{'Name': 'Beta', 'Type': 'PS1', 'Script': 'scripts/beta.ps1',
|
|
'DetectionMethod': 'Always'},
|
|
],
|
|
}
|
|
|
|
|
|
def _seed_and_publish(app, scopename='gea-shopfloor-cmm', manifest=None):
|
|
with app.app_context():
|
|
service.replace_scope_draft(scopename, 'runtime', manifest or SCOPE)
|
|
service.publish_scope(scopename, 'runtime', notes='initial')
|
|
service.db.session.commit()
|
|
|
|
|
|
def _mint_fetch_token(client, auth_headers):
|
|
resp = client.post('/api/apitokens',
|
|
json={'name': 'geenforce svc', 'scopes': ['geenforce.fetch']},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
return resp.get_json()['data']['secret']
|
|
|
|
|
|
def test_import_publish_serve(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
secret = _mint_fetch_token(client, auth_headers)
|
|
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret})
|
|
assert resp.status_code == 200, resp.data
|
|
body = json.loads(resp.data)
|
|
assert [e['Name'] for e in body['Applications']] == ['Alpha', 'Beta']
|
|
assert resp.headers.get('ETag')
|
|
assert resp.headers.get('X-Manifest-Version') == '1'
|
|
|
|
|
|
def test_etag_304(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
secret = _mint_fetch_token(client, auth_headers)
|
|
first = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret})
|
|
etag = first.headers['ETag']
|
|
again = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret, 'If-None-Match': etag})
|
|
assert again.status_code == 304
|
|
|
|
|
|
def test_draft_edit_does_not_change_served_bytes(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
secret = _mint_fetch_token(client, auth_headers)
|
|
before = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret}).data
|
|
|
|
# Edit the DRAFT (add an entry) but do NOT publish.
|
|
changed = {'Version': '2.6', 'Applications': SCOPE['Applications'] + [
|
|
{'Name': 'Gamma', 'Type': 'PS1', 'Script': 'scripts/gamma.ps1'}]}
|
|
with app.app_context():
|
|
service.replace_scope_draft('gea-shopfloor-cmm', 'runtime', changed)
|
|
service.db.session.commit()
|
|
|
|
after = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret}).data
|
|
assert before == after # served bytes come from the published snapshot only
|
|
|
|
|
|
def test_publish_then_rollback(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
secret = _mint_fetch_token(client, auth_headers)
|
|
v1 = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret}).data
|
|
|
|
changed = {'Version': '2.6', 'Applications': SCOPE['Applications'] + [
|
|
{'Name': 'Gamma', 'Type': 'PS1', 'Script': 'scripts/gamma.ps1'}]}
|
|
with app.app_context():
|
|
service.replace_scope_draft('gea-shopfloor-cmm', 'runtime', changed)
|
|
service.publish_scope('gea-shopfloor-cmm', 'runtime', notes='add gamma')
|
|
service.db.session.commit()
|
|
|
|
v2 = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret})
|
|
assert v2.headers['X-Manifest-Version'] == '2'
|
|
assert b'Gamma' in v2.data
|
|
|
|
with app.app_context():
|
|
service.rollback_scope('gea-shopfloor-cmm', 'runtime', 1)
|
|
service.db.session.commit()
|
|
|
|
rolled = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret})
|
|
assert rolled.data == v1 # byte-identical: published snapshots are frozen text
|
|
|
|
|
|
def test_unauthenticated_rejected(client, db, app):
|
|
_seed_and_publish(app)
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def _set_allowlist(app, value):
|
|
from shopdb.api import Setting
|
|
with app.app_context():
|
|
Setting.set('geenforce_allowed_cidrs', value, 'string', 'geenforce')
|
|
service.db.session.commit()
|
|
|
|
|
|
def test_ip_allowlist_allows_without_token(client, db, app):
|
|
# An allowlisted caller reaches the manifest with NO token (vault trust).
|
|
_seed_and_publish(app)
|
|
_set_allowlist(app, '127.0.0.0/8, 192.0.2.0/24') # test client is 127.0.0.1
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
|
|
assert resp.status_code == 200, resp.data
|
|
assert b'Alpha' in resp.data
|
|
|
|
|
|
def test_ip_not_in_allowlist_still_rejected(client, db, app):
|
|
# A caller outside the allowlist and with no token is refused (fail-closed).
|
|
_seed_and_publish(app)
|
|
_set_allowlist(app, '10.0.0.0/8') # test client 127.0.0.1 is NOT in 10/8
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_empty_allowlist_keeps_token_required(client, db, app):
|
|
# Empty/unset allowlist = disabled; token stays the only path (back-compat).
|
|
_seed_and_publish(app)
|
|
_set_allowlist(app, '')
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_spoofed_forwarded_for_does_not_bypass_allowlist(client, db, app):
|
|
# SECURITY: the allowlist uses remote_addr, not X-Forwarded-For. A caller
|
|
# whose real IP (127.0.0.1) is NOT allowlisted must NOT gain token-less access
|
|
# by forging X-Forwarded-For to an allowlisted address.
|
|
_seed_and_publish(app)
|
|
_set_allowlist(app, '192.0.2.0/24') # test client 127.0.0.1 is NOT in it
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-Forwarded-For': '192.0.2.10'})
|
|
assert resp.status_code == 401, 'spoofed X-Forwarded-For bypassed the allowlist'
|
|
|
|
|
|
def test_wrong_scope_rejected(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
resp = client.post('/api/apitokens',
|
|
json={'name': 'wrong', 'scopes': ['collector.ingest']},
|
|
headers=auth_headers)
|
|
secret = resp.get_json()['data']['secret']
|
|
manifest = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
|
headers={'X-API-Key': secret})
|
|
assert manifest.status_code == 401
|
|
|
|
|
|
def test_unpublished_scope_404(client, db, app, auth_headers):
|
|
with app.app_context():
|
|
service.replace_scope_draft('gea-shopfloor-keyence', 'runtime', SCOPE)
|
|
service.db.session.commit() # draft only, never published
|
|
secret = _mint_fetch_token(client, auth_headers)
|
|
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-keyence',
|
|
headers={'X-API-Key': secret})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_admin_list_and_preview(client, db, app, auth_headers):
|
|
_seed_and_publish(app)
|
|
listing = client.get('/api/geenforce/scopes', headers=auth_headers)
|
|
assert listing.status_code == 200
|
|
scopes = listing.get_json()['data']
|
|
cmm = next(s for s in scopes if s['scopename'] == 'gea-shopfloor-cmm')
|
|
assert cmm['entrycount'] == 2
|
|
assert cmm['publishedversion'] == 1
|
|
|
|
preview = client.get(f"/api/geenforce/scopes/{cmm['scopeid']}/preview",
|
|
headers=auth_headers)
|
|
assert preview.status_code == 200
|
|
assert [e['Name'] for e in preview.get_json()['data']['manifest']['Applications']] \
|
|
== ['Alpha', 'Beta']
|