Initial server source import
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import base64
|
||||
import builtins
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_DIR = ROOT / 'app'
|
||||
sys.path.insert(0, str(APP_DIR))
|
||||
|
||||
import dashboard
|
||||
import scanner_db
|
||||
from keycheckers.gcp import gcpKeycheck
|
||||
|
||||
|
||||
def der_tlv(tag, value):
|
||||
length = len(value)
|
||||
if length < 0x80:
|
||||
encoded_length = bytes([length])
|
||||
else:
|
||||
length_bytes = length.to_bytes((length.bit_length() + 7) // 8, 'big')
|
||||
encoded_length = bytes([0x80 | len(length_bytes)]) + length_bytes
|
||||
return bytes([tag]) + encoded_length + value
|
||||
|
||||
|
||||
def der_integer(value):
|
||||
value = value or b'\x00'
|
||||
if value[0] & 0x80:
|
||||
value = b'\x00' + value
|
||||
return der_tlv(0x02, value)
|
||||
|
||||
|
||||
def rsa_sequence(bits=2048, modulus=None, private_exponent=b'\x01'):
|
||||
modulus = modulus or (b'\x80' + b'\x01' * (bits // 8 - 1))
|
||||
values = [
|
||||
der_integer(b'\x00'),
|
||||
der_integer(modulus),
|
||||
der_integer(b'\x01\x00\x01'),
|
||||
der_integer(private_exponent),
|
||||
]
|
||||
values.extend(der_integer(b'\x03') for _ in range(5))
|
||||
return der_tlv(0x30, b''.join(values))
|
||||
|
||||
|
||||
def private_key_pem(bits=2048, pkcs8=False, modulus=None, private_exponent=b'\x01'):
|
||||
rsa_der = rsa_sequence(bits, modulus, private_exponent)
|
||||
if pkcs8:
|
||||
algorithm = der_tlv(
|
||||
0x30,
|
||||
der_tlv(0x06, gcpKeycheck.RSA_ENCRYPTION_OID) + der_tlv(0x05, b''),
|
||||
)
|
||||
der = der_tlv(0x30, der_integer(b'\x00') + algorithm + der_tlv(0x04, rsa_der))
|
||||
label = 'PRIVATE KEY'
|
||||
else:
|
||||
der = rsa_der
|
||||
label = 'RSA PRIVATE KEY'
|
||||
body = base64.b64encode(der).decode('ascii')
|
||||
return f'-----BEGIN {label}-----\n{body}\n-----END {label}-----\n'
|
||||
|
||||
|
||||
def credentials(pem):
|
||||
return {
|
||||
'client_email': 'fixture@example.invalid',
|
||||
'private_key_id': 'fixture-key-id',
|
||||
'private_key': pem,
|
||||
'project_id': 'fixture-project',
|
||||
'token_uri': gcpKeycheck.GOOGLE_TOKEN_URL,
|
||||
}
|
||||
|
||||
|
||||
class GCPRSABoundsTests(unittest.TestCase):
|
||||
def assert_rejected_without_cpu_or_network(self, pem):
|
||||
with mock.patch.object(builtins, 'pow') as rsa_pow, \
|
||||
mock.patch.object(gcpKeycheck.requests, 'post') as post:
|
||||
result = gcpKeycheck.check_service_account(credentials(pem), None, 5)
|
||||
self.assertEqual(result['status'], 'DEAD')
|
||||
self.assertEqual(result['classification'], 'invalid_private_key')
|
||||
rsa_pow.assert_not_called()
|
||||
post.assert_not_called()
|
||||
|
||||
def test_oversized_modulus_exponent_and_der_are_terminal_before_work(self):
|
||||
oversized_integer = b'\x01' * (gcpKeycheck.MAX_RSA_INTEGER_BYTES + 1)
|
||||
cases = {
|
||||
'modulus': private_key_pem(modulus=oversized_integer),
|
||||
'private_exponent': private_key_pem(private_exponent=oversized_integer),
|
||||
'der': (
|
||||
'-----BEGIN PRIVATE KEY-----\n'
|
||||
+ base64.b64encode(b'\x00' * (gcpKeycheck.MAX_DER_BYTES + 1)).decode('ascii')
|
||||
+ '\n-----END PRIVATE KEY-----\n'
|
||||
),
|
||||
}
|
||||
for name, pem in cases.items():
|
||||
with self.subTest(name=name):
|
||||
self.assert_rejected_without_cpu_or_network(pem)
|
||||
|
||||
def test_malformed_der_lengths_and_trailing_data_are_rejected(self):
|
||||
prefix = der_tlv(0x02, b'\x01' * 200)
|
||||
malformed = (
|
||||
der_tlv(0x30, prefix + b'\x30\x80'),
|
||||
der_tlv(0x30, prefix + b'\x30\x82\x01'),
|
||||
der_tlv(0x30, prefix + b'\x30\x83\x00\x00\x01'),
|
||||
der_tlv(0x30, prefix + b'\x30\x82\x40\x01'),
|
||||
rsa_sequence() + b'\x00',
|
||||
)
|
||||
for der in malformed:
|
||||
label = 'RSA PRIVATE KEY'
|
||||
pem = (
|
||||
f'-----BEGIN {label}-----\n'
|
||||
+ base64.b64encode(der).decode('ascii')
|
||||
+ f'\n-----END {label}-----\n'
|
||||
)
|
||||
with self.subTest(der=der[:8]):
|
||||
self.assert_rejected_without_cpu_or_network(pem)
|
||||
|
||||
def test_normal_pkcs1_2048_and_pkcs8_4096_reach_token_request(self):
|
||||
response = mock.Mock(status_code=400)
|
||||
response.json.return_value = {'error': 'invalid_grant'}
|
||||
response.text = 'invalid_grant'
|
||||
for bits, pkcs8 in ((2048, False), (4096, True)):
|
||||
with self.subTest(bits=bits, pkcs8=pkcs8), \
|
||||
mock.patch.object(builtins, 'pow', return_value=1) as rsa_pow, \
|
||||
mock.patch.object(gcpKeycheck.requests, 'post', return_value=response) as post:
|
||||
result = gcpKeycheck.check_service_account(
|
||||
credentials(private_key_pem(bits=bits, pkcs8=pkcs8)), None, 5
|
||||
)
|
||||
self.assertEqual(result['status'], 'DEAD')
|
||||
rsa_pow.assert_called_once()
|
||||
post.assert_called_once()
|
||||
|
||||
|
||||
class ADCResourceRedactionTests(unittest.TestCase):
|
||||
def test_enrichment_keeps_refresh_token_out_of_resource(self):
|
||||
sentinel = 'ADC-REFRESH-TOKEN-SENTINEL'
|
||||
finding = {
|
||||
'DetectorName': 'GCPApplicationDefaultCredentials',
|
||||
'Raw': sentinel,
|
||||
'ScannerContext': {'nearby': json.dumps({
|
||||
'type': 'authorized_user',
|
||||
'client_id': 'public-client-id',
|
||||
'client_secret': 'client-secret',
|
||||
'refresh_token': sentinel,
|
||||
'quota_project_id': 'quota-project',
|
||||
})},
|
||||
}
|
||||
enriched = scanner_db.enrich_finding(finding)
|
||||
self.assertEqual(enriched['project_id'], 'quota-project')
|
||||
self.assertEqual(enriched['resource'], '')
|
||||
self.assertNotIn(sentinel, json.dumps(enriched))
|
||||
|
||||
finding['PostmanContext'] = {
|
||||
'credential_kind': 'application_default_credentials',
|
||||
'json_path': sentinel,
|
||||
}
|
||||
self.assertEqual(scanner_db.enrich_finding(finding)['resource'], '')
|
||||
|
||||
def test_render_redacts_only_adc_resource_rows(self):
|
||||
sentinel = 'HISTORICAL-ADC-REFRESH-TOKEN'
|
||||
frame = pd.DataFrame([
|
||||
{
|
||||
'detector_name': 'GCPApplicationDefaultCredentials',
|
||||
'credential_kind': '',
|
||||
'resource': sentinel,
|
||||
},
|
||||
{
|
||||
'detector_name': 'ImportedDetector',
|
||||
'credential_kind': 'application_default_credentials',
|
||||
'resource': sentinel + '-IMPORTED',
|
||||
},
|
||||
{
|
||||
'detector_name': 'AzureOpenAI',
|
||||
'credential_kind': 'azure_openai_key',
|
||||
'resource': 'azure-resource-name',
|
||||
},
|
||||
{
|
||||
'detector_name': 'GCP',
|
||||
'credential_kind': 'service_account',
|
||||
'resource': 'nonsecret-private-key-id',
|
||||
},
|
||||
])
|
||||
with mock.patch.object(dashboard.st, 'dataframe') as render:
|
||||
dashboard.display_df(frame)
|
||||
rendered = render.call_args.args[0]
|
||||
rendered_text = rendered.to_string()
|
||||
self.assertNotIn(sentinel, rendered_text)
|
||||
self.assertIn('azure-resource-name', rendered_text)
|
||||
self.assertIn('nonsecret-private-key-id', rendered_text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user