Skip to content

Implement OWASP security logging.#6689

Open
blackboxsw wants to merge 31 commits intocanonical:mainfrom
blackboxsw:owasp-logs-poc
Open

Implement OWASP security logging.#6689
blackboxsw wants to merge 31 commits intocanonical:mainfrom
blackboxsw:owasp-logs-poc

Conversation

@blackboxsw
Copy link
Collaborator

@blackboxsw blackboxsw commented Jan 26, 2026

Provide an initial spike for logging OWASP formatted events in cloud-init for discussion. Integration tests will be added upon agreement for security logging procedures.

Proposed Commit Message

    feat: add OWASP security event logging for user creation and system restart
    
    Implement OWASP structured logs in /var/log/cloud-init-security.log.
    
    Add security-related operations performed by cloud-init on behalf
    of user-data or platform meta-data:
    - user creation
    - user password change
    - system restart
    - system shutdown
  
    Default security log file can be changed by setting an alternative value
    for security_log_file in /etc/cloud/cloud.cfg(.d/*.cfg).

Additional Context

A followup PR will provide USER update functionality due to groups: { admingrp: [root, sys]} because this will require a refactor of Distro.create_group for multiple distros

Test Steps

CLOUD_INIT_CLOUD_INIT_OS_IMAGE=resolute tox -e integration-tests -- tests/integration_tests/modules/test_combined.py::TestCombined::test_security_logs

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

@github-actions github-actions bot added the documentation This Pull Request changes documentation label Jan 26, 2026
@blackboxsw blackboxsw requested a review from holmanb January 26, 2026 21:48
@blackboxsw blackboxsw assigned blackboxsw and holmanb and unassigned blackboxsw Jan 26, 2026
@blackboxsw
Copy link
Collaborator Author

cc: @dbungert

Copy link
Member

@holmanb holmanb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @blackboxsw. First pass.

If the goal is for this to be cross-distro, then implementing this in the distro class doesn't seem optimal because distro methods are expected to be (and in this case are) overridden.

Also, is there a reason that the logger package cannot be used? Manually writing to files seems undesirable - I would have expected this to define a custom logger for this purpose.

Thank you for the type annotations. Besides just annotating the types, I'd like to see if we can also avoid unnecessary complexity - something that annotations can assist with. For example rather than checking an Optional parameter for trutheyness, we could instead avoid the possibility of None and pass a falsey value instead.

Comment on lines +825 to +829
sec_log_user_created(
userid="cloud-init",
new_userid=name,
attributes={"snapuser": True, "sudo": True},
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intermixing code of different different purposes like this will lead to difficulty maintaining and auditing the code. I would prefer if we could find a cleaner design.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, let's go decorator route instead,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decorators didn't address the problem. These decorators are still mixing code of different purposes.

if params:
# Filter out None values and convert to strings
filtered_params = [str(p) for p in params if p is not None]
if filtered_params:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional is unnecessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unresolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the event that all filtered_params are empty. We don't want a trailing ":" appended if filtered_params == []

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the event that all filtered_params are empty.

This should never happen. See my comment above.

@holmanb holmanb added the incomplete Action required by submitter label Feb 10, 2026
@github-actions github-actions bot added stale-pr Pull request is stale; will be auto-closed soon and removed stale-pr Pull request is stale; will be auto-closed soon labels Feb 25, 2026
@blackboxsw blackboxsw force-pushed the owasp-logs-poc branch 2 times, most recently from 046db76 to e7168c0 Compare March 2, 2026 16:44
@blackboxsw blackboxsw removed the incomplete Action required by submitter label Mar 2, 2026
@blackboxsw blackboxsw changed the title POC: Implement OWASP security logging. Implement OWASP security logging. Mar 5, 2026
@blackboxsw blackboxsw requested a review from holmanb March 5, 2026 05:19
@blackboxsw
Copy link
Collaborator Author

I believe I have addressed all comments and this is ready for re-review.

…estart

Implement OWASP structured logs in /var/log/cloud-init-security.log.

Add security-related operations performed by cloud-init on behalf
of user-data or platform meta-data:
- user creation
- user password change
- system restart
- system shutdown

Default security log file can be changed by setting an alternative value
for security_log_file in /etc/cloud/cloud.cfg(.d/*.cfg).
Replace LevelBasedFormatter with handler-level filters. SecurityOnlyFilter
and NoSecurityFilter are applied at the handler level so SECURITY records
flow only to a dedicated FileHandler on cloud-init-output.log using
SECURITY_LOG_FORMAT, and are blocked from stderr and LogExporter.

setup_security_logging() silently no-ops on OSError so unprivileged
test environments are unaffected. Update test_logger_prints_security_as_
json_lines to assert file output and absence from stderr.
Use netdev_info to discover any interface with a global scope IP address.
If no IPv4 address is present, return IPv6. If no network devices are
up or no global addresses are present, avoid adding host_ip to event
logs.

Additionally, add "type": "security" to all events.
Comment on lines 1105 to 1129
@@ -1113,6 +1123,7 @@ def set_passwd(self, user, passwd, hashed=False):

return True

@sec_log_password_changed_batch
def chpasswd(self, plist_in: list, hashed: bool):
payload = (
"\n".join(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add @final here too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also migrated the sec_log_user_created decorator to add_user and add_snap_user as that is actually where a user gets created. Added @final decorators to both of those methods.
Refactored subclasses of add_user behavior into _add_user_preprocess_kwargs, _build_add_user_cmd and _post_add_user methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still overridden in a child class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in distros/bsd.py, but bsd directly calls set_passwd for each name in plist_in. So BSD will still be logging separate OWASP events for each user, just as our @sec_log_password_changed_batch will.

As a result, I don't want to decorate chpasswd with @final and I also don't want to decorate cloudinit.distros.bsd.Disto.chpasswd with @sec_log passwd_changed_batch

:return: Dictionary containing the security event data.
"""
event = {
"datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also concerns me - I would rather avoid side-effects in logging libraries. This causes a syscall.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it seems odd to be manually building building a format for the log here.

Python's builtin logger allows supplying custom Formatter objects - which is where I would expect the format to be defined. The formatter should already have the time and can be configured to the desired format.

"""A decorator logging a system shutdown event."""

@functools.wraps(func)
def decorator(*args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this use kwargs when the keyword names are known?

@canonical canonical deleted a comment from github-actions bot Mar 10, 2026
- drop unintended @Final from Distro.set_passwd
- simplify sec_log decorators to assume positional param idx 1 when kwarg
  empty
- drop unchecked bool return values from _post_add_user
@blackboxsw blackboxsw requested a review from holmanb March 10, 2026 19:10
Copy link
Member

@holmanb holmanb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @blackboxsw. See the latest feedback.

In general I'd like to see us avoid iterating over interfaces, opening sockets, and calling datetime on a per-log basis - and these kinds of side-effects don't seem well-suited for a system logging library.

The same goes for parsing data from untyped Dicts. A major problem with building a solution that can be maintained is that distro-specific code is doing configuration validation and manipulation - the existing code doesn't have clear separation of concerns.

Also: I'd like to see a sample log produced by this code.

}
@final
@sec_log_user_created
def add_user(self, name, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This no longer has a type annotation. Why did the type signature change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have migrated this annotation to express that we expect no return value. Given that this PR migrates the pre-flight check for pre-existing user outside the Distro.add_user` call, there is no longer a need to check a bool return code from add_user given the simplification to the method.

Now we only call add user if we are sure the user doesn't exist up in create_user.

        pre_existing_user = util.is_user(name)
        if pre_existing_user:
            LOG.info("User %s already exists, skipping.", name)
        else:
            self.add_user(name, **kwargs)

We also don't have any direct callers to Distro.add_user in any cloud-init runtime operations. Any call-sites in upstream are invoked via the Distro.create_user method. This could expose some downstream images with custom private Distro implementations, but that is a highly unlikely scenario as we don't treat cloudinit as a python library supporting direct calls into our python modules and class APIs.

Comment on lines 1105 to 1129
@@ -1113,6 +1123,7 @@ def set_passwd(self, user, passwd, hashed=False):

return True

@sec_log_password_changed_batch
def chpasswd(self, plist_in: list, hashed: bool):
payload = (
"\n".join(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still overridden in a child class.

def shutdown_command(cls, *, mode, delay, message):
# called from cc_power_state_change.load_power_state
# called from cc_power_state_change.load_power_state and clean
if hasattr(cls, "_shutdown_command"): # Overridden in alpine.Distro
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class uses inheritance to define and override default behaviors. It seems unusual to reach for a low-level meta-programming utility rather than using inheritance. Why was this chosen instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent was to leave us with only one place with sec_log decorators in the parent class and disallow creating the shutdown_command method on subclasses via @final. I'll refactor this to use the same approach we user for add user with _build_shutdown_cmd. I've refactored the validation of the delay value which was duplicated in alpine and pulled it into a pre-flight check in shutdown_command.

LOG.info("Added user '%s' to group '%s'", member, name)

def shutdown_command(self, mode="poweroff", delay="now", message=None):
@classmethod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

for key, val in kwargs.items():
if key in pw_useradd_opts and val and isinstance(val, (str, int)):
pw_useradd_cmd.extend([pw_useradd_opts[key], str(val)])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace

Comment on lines +206 to +208
userid = kwargs.get("user")
if not userid:
userid = args[1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fishy. Isn't args[1] the password?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the signature to expect known positional args. No more kwargs parsing.

if params:
# Filter out None values and convert to strings
filtered_params = [str(p) for p in params if p is not None]
if filtered_params:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unresolved

Comment on lines +154 to +166
params = ["cloud-init", new_userid]
groups_msg = ""
groups_suffix = kwargs.get("groups", "")
if groups_suffix:
if isinstance(groups_suffix, (dict, list)):
groups_suffix = ",".join(groups_suffix)
for perms in ("sudo", "doas"):
if kwargs.get(perms):
groups_suffix += f",{perms}"
if groups_suffix:
groups_suffix = groups_suffix.strip(",")
groups_msg = f" in groups: {groups_suffix}"
params.append(f"groups:{groups_suffix}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem ideal. This is interpreting arbitrary untyped data from a dict. This dict interpretation is a long ways from wherever this datastructure is defined, yet it is tightly coupled to the structure where it was defined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, this feels fragile and is working around the complexity in both add_user and _build_add_user_cmd which both attempt to parse and handle "groups" being passed as an untyped dict config key value from merged user-data. An alternative to keep things closer to where distros processes this config content would be a helper function in cloudinit/distros/init.py which can expose the properly typed groups content in order to ensure the, add_user calls, _build_add_user_cmd and log decorator properly handle this information in the same manner. There is a lot of duplication here in the decorator and in Distro instance methods that we can avoid if all call-sites get a typed response from one place.

Comment on lines +185 to +187
plist_in = kwargs.get("plist_in")
if not plist_in:
plist_in = args[1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This construct seems confusing, and possibly wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped in favor of known positional arg plist_in

Comment on lines +825 to +829
sec_log_user_created(
userid="cloud-init",
new_userid=name,
attributes={"snapuser": True, "sudo": True},
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decorators didn't address the problem. These decorators are still mixing code of different purposes.

Add _build_shutdown_cmd which is subclassed in Alpine.
Move pre-flight delay type checking into Distro.shutdown_command and
call the  _build_shutdown_cmd to construct the distro-specific command.
Copy link
Collaborator Author

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed most review points, still looking at:

  1. adding a Distro.extract_group method to extract typed information from opaque kwargs config passed into add_user
  2. looking at custom logging formatted to provide the logging timestamp instead of calling datetime directly.

"event": _build_event_string(event_type, event_params),
"level": level.value,
"description": description,
"hostname": util.get_hostname(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uncertain how we should resolve this concern and also deliver identifiable information within each log to provide breadcrumbs for security log aggregators about the source of the log.

Do you think we may instead want to just extract provided hostname data from the Platform via a call to util.get_hostname_fqdn to avoid using socket.gethostname. Or possibly @lru_cache around get_hostname to avoid recurring cost of a call?

I see our security logs as only being on the order of 10 events per boot vs 100's. So I don't know whether you are concerned about recurring cost, or just that fact that logging reaches out to some other system which could timeout.

It's possible we could drop this key, which places a responsibility on all security log aggregators to identify the source from which all logs originate.

Comment on lines +154 to +166
params = ["cloud-init", new_userid]
groups_msg = ""
groups_suffix = kwargs.get("groups", "")
if groups_suffix:
if isinstance(groups_suffix, (dict, list)):
groups_suffix = ",".join(groups_suffix)
for perms in ("sudo", "doas"):
if kwargs.get(perms):
groups_suffix += f",{perms}"
if groups_suffix:
groups_suffix = groups_suffix.strip(",")
groups_msg = f" in groups: {groups_suffix}"
params.append(f"groups:{groups_suffix}")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, this feels fragile and is working around the complexity in both add_user and _build_add_user_cmd which both attempt to parse and handle "groups" being passed as an untyped dict config key value from merged user-data. An alternative to keep things closer to where distros processes this config content would be a helper function in cloudinit/distros/init.py which can expose the properly typed groups content in order to ensure the, add_user calls, _build_add_user_cmd and log decorator properly handle this information in the same manner. There is a lot of duplication here in the decorator and in Distro instance methods that we can avoid if all call-sites get a typed response from one place.

if params:
# Filter out None values and convert to strings
filtered_params = [str(p) for p in params if p is not None]
if filtered_params:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the event that all filtered_params are empty. We don't want a trailing ":" appended if filtered_params == []

}
@final
@sec_log_user_created
def add_user(self, name, **kwargs):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have migrated this annotation to express that we expect no return value. Given that this PR migrates the pre-flight check for pre-existing user outside the Distro.add_user` call, there is no longer a need to check a bool return code from add_user given the simplification to the method.

Now we only call add user if we are sure the user doesn't exist up in create_user.

        pre_existing_user = util.is_user(name)
        if pre_existing_user:
            LOG.info("User %s already exists, skipping.", name)
        else:
            self.add_user(name, **kwargs)

We also don't have any direct callers to Distro.add_user in any cloud-init runtime operations. Any call-sites in upstream are invoked via the Distro.create_user method. This could expose some downstream images with custom private Distro implementations, but that is a highly unlikely scenario as we don't treat cloudinit as a python library supporting direct calls into our python modules and class APIs.

def shutdown_command(cls, *, mode, delay, message):
# called from cc_power_state_change.load_power_state
# called from cc_power_state_change.load_power_state and clean
if hasattr(cls, "_shutdown_command"): # Overridden in alpine.Distro
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent was to leave us with only one place with sec_log decorators in the parent class and disallow creating the shutdown_command method on subclasses via @final. I'll refactor this to use the same approach we user for add user with _build_shutdown_cmd. I've refactored the validation of the delay value which was duplicated in alpine and pulled it into a pre-flight check in shutdown_command.

for _, info in netdev_info().items():
if not info["up"]:
continue
ipv4: List[dict] = info.get("ipv4", [])
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped cloudinit.netinfo.get_host_ip function to avoid the cost of shelling out to ip addr --json for every security event log. So this comment no longer applies.

This "up" check on the device would have also been a prerequisite to surfacing valid ipv4 settings. I also agree that a down interface will also not have any valid ipv4 of ipv6 addresses, so this check was unnecessary.

Agreed on typing in netinfo, and I think that is already tracked as an existing enhancement #5445 to eventually resolve those issues.

"""
try:
first_ipv6: Optional[str] = None
for _, info in netdev_info().items():
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this if we end up adding host_ip info in the future.

"event": _build_event_string(event_type, event_params),
"level": level.value,
"description": description,
"hostname": util.get_hostname(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OWASP log formats look for identifiable information in the individual logs to ease the burden on aggregators. To avoid calling to socket.gethostname(). We could instead call util.get_hostname_fqdn to get what the IMDS told us the hostname should be (which is run in cloud-init-local stage to grab this data from the IMDS before cloud-init. We could also use @lru_cache to avoid repeated cost for such an operation.

Comment on lines +206 to +208
userid = kwargs.get("user")
if not userid:
userid = args[1]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the signature to expect known positional args. No more kwargs parsing.

Comment on lines 1105 to 1129
@@ -1113,6 +1123,7 @@ def set_passwd(self, user, passwd, hashed=False):

return True

@sec_log_password_changed_batch
def chpasswd(self, plist_in: list, hashed: bool):
payload = (
"\n".join(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in distros/bsd.py, but bsd directly calls set_passwd for each name in plist_in. So BSD will still be logging separate OWASP events for each user, just as our @sec_log_password_changed_batch will.

As a result, I don't want to decorate chpasswd with @final and I also don't want to decorate cloudinit.distros.bsd.Disto.chpasswd with @sec_log passwd_changed_batch

Comment on lines +72 to +73
# Filter out None values and convert to strings
filtered_params = [str(p) for p in params if p is not None]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the type annotation, the filter will never do anything.

if params:
# Filter out None values and convert to strings
filtered_params = [str(p) for p in params if p is not None]
if filtered_params:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the event that all filtered_params are empty.

This should never happen. See my comment above.


def sec_log_password_changed_batch(func):
@functools.wraps(func)
def decorator(self, plist_in, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get a type annotation on plist_in?

"""A decorator logging a password change event."""

@functools.wraps(func)
def decorator(self, user: str, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function that this decorates doesn't have an annotation. Can we add it there too please?

event_type=OWASPEventType.AUTHN_PASSWORD_CHANGE,
level=OWASPEventLevel.INFO,
description=f"Password changed for user '{user}'",
event_params=["cloud-init", user],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is every event_params getting passed ["cloud-init", ...]? If this is to be added by default, this doesn't seem like the right place for it, since it has to be duplicated at every call site.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved into _log_security_event to simplify call-sites.

interface: str,
dhcp_log_func: Optional[Callable[[str, str, str], None]] = None,
distro=None,
metric=None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped unrelated stay delta from separate PR.

- :file:`/var/log/cloud-init.log`: The primary log file. Verbose, but useful.
- :file:`/var/log/cloud-init-output.log`: Captures the output from each stage.
Output from user scripts goes here.
- :file:`/var/log/cloud-init-security.log`: This file logs OWASP security
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files don't log events.

"""A decorator to log a user creation event and group attributes."""

@functools.wraps(func)
def decorator(self, name, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an annotation for name.

# User creation operation did not raise an Exception
_log_security_event(
event_type=OWASPEventType.USER_CREATED,
level=OWASPEventLevel.WARN,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this level WARN?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's defined as WARN level on Canonical SSDLC user event guidance

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens due to trusted inputs and this will case a WARN on every boot - the typically arguments for "why" to WARN don't seem to apply in this case.

Also, since that is just an example this seems up for interpretation. I think it makes more sense not to warn on every boot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just set it to INFO and include a short comment in case anyone wonders why.

Comment on lines +120 to +130
groups_suffix = kwargs.get("groups", "")
if groups_suffix:
if isinstance(groups_suffix, (dict, list)):
groups_suffix = ",".join(groups_suffix)
for perms in ("sudo", "doas"):
if kwargs.get(perms):
groups_suffix += f",{perms}"
if groups_suffix:
groups_suffix = groups_suffix.strip(",")
groups_msg = f" in groups: {groups_suffix}"
params.append(f"groups:{groups_suffix}")
Copy link
Member

@holmanb holmanb Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere, this does both kwarg parsing and manipulation of data types that have nothing to do with logging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both Distro._user_groups_to_list and Distro._get_elevated_roles to perform that processing in Distro and avoid pushing that kwarg handling into log/security_event_log.

@blackboxsw blackboxsw force-pushed the owasp-logs-poc branch 2 times, most recently from 0e7b9f1 to 571b53b Compare March 21, 2026 15:09
Also call Distro._user_groups_to_list in sec_log_user_created to
avoid duplicated processing of add_user kwargs in security_event_log.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation This Pull Request changes documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants