Skip to content

Commit 7b3fbda

Browse files
committed
feat(audit): integrate resource audit delivery
1 parent 81f1c91 commit 7b3fbda

12 files changed

Lines changed: 368 additions & 4 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ jobs:
2727
- uses: actions/checkout@v7
2828
with:
2929
fetch-depth: 50
30+
- name: Checkout Audit SDK dependency
31+
uses: actions/checkout@v7
32+
with:
33+
repository: exordos/gcl_sdk
34+
ref: agent/audit-delivery
35+
path: gcl_sdk
3036
- name: Build Exordos Core
3137
env:
3238
PUSH_CFG: ${{ secrets.PUSH_CFG }}
39+
LOCAL_GENESIS_SDK_PATH: ${{ github.workspace }}/gcl_sdk
3340
run: |
3441
set -eux
3542
mkdir -p ${ARTIFACTS_PATH}

.github/workflows/tests.yml

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ jobs:
4242
strategy:
4343
fail-fast: true
4444
matrix:
45-
python-version: ["3.10", "3.12", "3.14"]
45+
include:
46+
- python-version: "3.10"
47+
tox-env: py310
48+
- python-version: "3.12"
49+
tox-env: py312
50+
- python-version: "3.14"
51+
tox-env: py314
4652
services:
4753
postgresql:
4854
image: postgres:latest
@@ -64,6 +70,12 @@ jobs:
6470
- 5432:5432
6571
steps:
6672
- uses: actions/checkout@v7
73+
- name: Checkout Audit SDK dependency
74+
uses: actions/checkout@v7
75+
with:
76+
repository: exordos/gcl_sdk
77+
ref: agent/audit-delivery
78+
path: gcl_sdk
6779
- name: Set up Python ${{ matrix.python-version }}
6880
uses: actions/setup-python@v6
6981
with:
@@ -76,12 +88,26 @@ jobs:
7688
run: uv tool install tox --with tox-uv
7789
- name: Unit tests
7890
run: |
79-
tox -e ${{ matrix.python-version }}
91+
tox -e ${{ matrix.tox-env }} --notest
92+
uv pip install \
93+
--python .tox/${{ matrix.tox-env }}/bin/python \
94+
--no-deps \
95+
--editable "${{ github.workspace }}/gcl_sdk"
96+
.tox/${{ matrix.tox-env }}/bin/coverage run \
97+
-p -m pytest --timer-top-n=10 -n 10 \
98+
exordos_core/tests/unit
8099
- name: Functional tests
81100
env:
82101
DATABASE_URI: "postgresql://exordos_core:exordos_core@localhost:5432/exordos_core"
83102
run: |
84-
tox -e ${{ matrix.python-version }}-functional
103+
tox -e ${{ matrix.tox-env }}-functional --notest
104+
uv pip install \
105+
--python .tox/${{ matrix.tox-env }}-functional/bin/python \
106+
--no-deps \
107+
--editable "${{ github.workspace }}/gcl_sdk"
108+
.tox/${{ matrix.tox-env }}-functional/bin/coverage run \
109+
-p -m pytest --timer-top-n=10 -n 10 \
110+
exordos_core/tests/functional
85111
Coverage:
86112
runs-on: ubuntu-24.04
87113
strategy:
@@ -90,6 +116,12 @@ jobs:
90116
python-version: ["3.12"]
91117
steps:
92118
- uses: actions/checkout@v7
119+
- name: Checkout Audit SDK dependency
120+
uses: actions/checkout@v7
121+
with:
122+
repository: exordos/gcl_sdk
123+
ref: agent/audit-delivery
124+
path: gcl_sdk
93125
- name: Set up Python ${{ matrix.python-version }}
94126
uses: actions/setup-python@v6
95127
with:
@@ -102,4 +134,13 @@ jobs:
102134
run: uv tool install tox --with tox-uv
103135
- name: Coverage
104136
run: |
105-
tox -e begin,${{ matrix.python-version }},end
137+
tox -e begin
138+
tox -e py312 --notest
139+
uv pip install \
140+
--python .tox/py312/bin/python \
141+
--no-deps \
142+
--editable "${{ github.workspace }}/gcl_sdk"
143+
.tox/py312/bin/coverage run \
144+
-p -m pytest --timer-top-n=10 -n 10 \
145+
exordos_core/tests/unit
146+
tox -e end

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ tox -e ruff-check # Check code style
9999
tox -e mypy # Type checking
100100
```
101101

102+
Do not store individual test-run reports, dated stand snapshots, or execution
103+
history in this repository. Keep only current test plans, runbooks, test IDs,
104+
automated tests, acceptance criteria, and concise coverage status here. Store
105+
sanitized historical run reports in
106+
`github/exordos/cassi/docs/test-runs/exordos-core/` and never include secrets,
107+
production payloads, full logs, databases, or binary artifacts.
108+
102109
### Build Commands
103110

104111
```bash

etc/exordos_core/exordos_core.conf.j2

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,14 @@ enabled = False
5656
# exordos_notification element, which drops an [events] override into
5757
# /etc/exordos_core/exordos_core.d/ (loaded via --config-dir by ec-user-api).
5858

59+
[audit_delivery]
60+
enabled = False
61+
# endpoint = http://audit.local.genesis-core.tech:8080/
62+
# api_version = v1
63+
# auth_token =
64+
# timeout = 5
65+
# batch_size = 100
66+
#
67+
# The separate core_audit element enables the worker by installing a
68+
# config drop-in under /etc/exordos_core/exordos_core.d/ and restarting
69+
# ec-gservice. Installing the central audit element alone does not enable it.

exordos/core-audit.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2026 Genesis Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License.
5+
6+
# Build the Core-to-Audit integration separately because the rendered output
7+
# contains the manually supplied bearer token. Keep the output protected and
8+
# remove it after deploy.
9+
build:
10+
elements:
11+
- manifest: manifests/core_audit.yaml.j2
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2026 Genesis Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License.
5+
6+
name: "core_audit"
7+
description: "Configure Exordos Core audit delivery"
8+
schema_version: 1
9+
version: "{{ version | default('1.0.0') }}"
10+
api_version: "v1"
11+
12+
requirements:
13+
audit:
14+
from_version: "0.0.0"
15+
16+
resources:
17+
$core.config.configs:
18+
core_audit_config:
19+
project_id: "12345678-c625-4fee-81d5-f691897b8142"
20+
path: /etc/exordos_core/exordos_core.d/90-audit-delivery.conf
21+
mode: "0600"
22+
owner: root
23+
group: root
24+
target:
25+
kind: node_set
26+
# core_set has a stable UUID in the core element manifest.
27+
node_set: "70c88222-b4d9-46c3-9340-aa5bfaaa4b94"
28+
on_change:
29+
kind: shell
30+
command: systemctl restart ec-gservice
31+
body:
32+
kind: text
33+
content: |
34+
[audit_delivery]
35+
enabled = true
36+
endpoint = http://audit.local.genesis-core.tech:8080/
37+
api_version = v1
38+
auth_token = {{ audit_token }}
39+
timeout = 5
40+
batch_size = 100
41+
42+
# The token is supplied manually until exordos_core#470 adds declarative
43+
# service-token issuance and rotation. Do not add exports: this integration
44+
# element owns no public resource contract.

exordos_core/cmd/gservice.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import logging
1818
import sys
1919

20+
from gcl_sdk.audit import opts as audit_opts
2021
from gcl_sdk.events import opts as sdk_opts
2122
from oslo_config import cfg
2223
from restalchemy.common import config_opts as ra_config_opts
@@ -40,6 +41,7 @@
4041
CONF = cfg.CONF
4142
ra_config_opts.register_posgresql_db_opts(CONF)
4243
sdk_opts.register_event_opts(CONF)
44+
audit_opts.register_audit_delivery_opts(CONF)
4345

4446
CONF.register_cli_opts(cli_opts, DOMAIN)
4547

exordos_core/compute/dm/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from gcl_sdk.agents.universal.api import crypto as ua_crypto
2222
from gcl_sdk.agents.universal.dm import models as ua_models
23+
from gcl_sdk.audit.dm import models as audit_models
2324
from gcl_sdk.infra.dm import models as infra_models
2425
import netaddr
2526
from restalchemy.dm import filters as dm_filters
@@ -299,11 +300,14 @@ def set_active(self):
299300

300301

301302
class Node(
303+
audit_models.AuditLogSQLStorableMixin,
302304
infra_models.Node,
303305
orm.SQLStorableWithJSONFieldsMixin,
304306
):
305307
__tablename__ = "nodes"
306308
__jsonfields__ = ["default_network"]
309+
__audit_service_name__ = nc.POLICY_SERVICE_NAME
310+
__audit_resource_type__ = "node"
307311

308312
uuid = properties.property(
309313
types.UUID(),

exordos_core/gservice/service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from gcl_sdk.agents.universal.drivers import core as ua_core_drivers
2626
from gcl_sdk.agents.universal.services import agent as ua_agent_service
2727
from gcl_sdk.agents.universal.services import scheduler as ua_scheduler_service
28+
from gcl_sdk.audit import opts as audit_opts
29+
from gcl_sdk.audit.services import senders as audit_senders
2830
from gcl_sdk.events import constants as event_c
2931
from gcl_sdk.events.services import senders
3032
from oslo_config import cfg
@@ -194,6 +196,12 @@ def __init__(self, iter_min_period=3, iter_pause=0.1):
194196
event_sender = senders.EventSenderService.build_from_config()
195197
else:
196198
event_sender = None
199+
if audit_opts.get_audit_delivery_config().enabled:
200+
audit_sender = audit_senders.AuditSenderService.build_from_config(
201+
iter_min_period=iter_min_period,
202+
)
203+
else:
204+
audit_sender = None
197205
em_builder = em_builders.ElementManagerBuilder(iter_min_period=iter_min_period)
198206
janitor = janitor_service.ExpiredEmailConfirmationCodeJanitorService(
199207
iter_min_period=60 * 60,
@@ -233,6 +241,8 @@ def __init__(self, iter_min_period=3, iter_pause=0.1):
233241
]
234242
if event_sender is not None:
235243
self._services.append(event_sender)
244+
if audit_sender is not None:
245+
self._services.append(audit_sender)
236246
self._next_run_times = {id(s): 0 for s in self._services}
237247

238248
def _setup(self):
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2026 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import typing as tp
18+
import uuid as sys_uuid
19+
20+
from gcl_iam.tests.functional import clients as iam_clients
21+
22+
23+
class TestAuditApi:
24+
def test_node_audit_lifecycle(
25+
self,
26+
node_factory: tp.Callable,
27+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
28+
auth_user_admin: iam_clients.GenesisCoreAuth,
29+
):
30+
client = user_api_client(auth_user_admin)
31+
node = node_factory()
32+
node_url = client.build_collection_uri(["compute", "nodes"])
33+
34+
response = client.post(node_url, json=node)
35+
assert response.status_code == 201
36+
37+
resource_url = client.build_resource_uri(
38+
["compute", "nodes", node["uuid"]]
39+
)
40+
response = client.put(resource_url, json={"cores": 2, "ram": 2048})
41+
assert response.status_code == 200
42+
43+
response = client.delete(resource_url)
44+
assert response.status_code == 204
45+
46+
response = client.get(
47+
client.build_collection_uri(["security", "audit"]),
48+
params={"resource_uuid": node["uuid"]},
49+
)
50+
assert response.status_code == 200
51+
52+
events = response.json()
53+
assert len(events) == 3
54+
assert {event["action"] for event in events} == {
55+
"create",
56+
"update",
57+
"delete",
58+
}
59+
60+
events_by_action = {event["action"]: event for event in events}
61+
for event in events:
62+
assert event["service_name"] == "compute"
63+
assert event["resource_type"] == "node"
64+
assert event["resource_uuid"] == node["uuid"]
65+
assert event["project_id"] == node["project_id"]
66+
assert event["actor_user_uuid"] == auth_user_admin.uuid
67+
68+
assert events_by_action["create"]["snapshot"]["cores"] == 1
69+
assert events_by_action["update"]["snapshot"]["cores"] == 2
70+
assert events_by_action["update"]["snapshot"]["ram"] == 2048
71+
assert events_by_action["delete"]["snapshot"] is None
72+
73+
def test_audit_events_are_project_scoped(
74+
self,
75+
node_factory: tp.Callable,
76+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
77+
auth_user_admin: iam_clients.GenesisCoreAuth,
78+
auth_test1_p1_user: iam_clients.GenesisCoreAuth,
79+
auth_test2_p1_user: iam_clients.GenesisCoreAuth,
80+
):
81+
admin_client = user_api_client(auth_user_admin)
82+
nodes_url = admin_client.build_collection_uri(["compute", "nodes"])
83+
node_a = node_factory(project_id=sys_uuid.UUID(auth_test1_p1_user.project_id))
84+
node_b = node_factory(project_id=sys_uuid.UUID(auth_test2_p1_user.project_id))
85+
86+
assert admin_client.post(nodes_url, json=node_a).status_code == 201
87+
assert admin_client.post(nodes_url, json=node_b).status_code == 201
88+
89+
project_client = user_api_client(
90+
auth_test1_p1_user,
91+
permissions=["audit.events.read"],
92+
project_id=auth_test1_p1_user.project_id,
93+
)
94+
response = project_client.get(
95+
project_client.build_collection_uri(["security", "audit"]),
96+
)
97+
98+
assert response.status_code == 200
99+
assert {event["resource_uuid"] for event in response.json()} == {
100+
node_a["uuid"]
101+
}
102+
103+
for node in (node_a, node_b):
104+
admin_client.delete(
105+
admin_client.build_resource_uri(
106+
["compute", "nodes", node["uuid"]]
107+
)
108+
)

0 commit comments

Comments
 (0)