Skip to content

Commit 43b37a6

Browse files
committed
Make security and filesystem checks return predicate results
1 parent bc319f8 commit 43b37a6

7 files changed

Lines changed: 135 additions & 56 deletions

File tree

docs/plugins/generated.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7927,7 +7927,7 @@ with:
79277927

79287928
### `security.authselect.check`
79297929

7930-
Assert the current authselect profile and enabled features on RHEL-like systems.
7930+
Check the current authselect profile and enabled features on RHEL-like systems.
79317931

79327932
- Remote session: `true`
79337933
- Dry-run support: `true`
@@ -8511,7 +8511,7 @@ with:
85118511

85128512
### `security.pki.cert.expiry_check`
85138513

8514-
Assert that a certificate remains valid for at least min_days.
8514+
Check whether a certificate remains valid for at least min_days.
85158515

85168516
- Remote session: `true`
85178517
- Dry-run support: `true`
@@ -8645,7 +8645,7 @@ with:
86458645

86468646
### `security.pki.cert.issuer_check`
86478647

8648-
Assert that a certificate issuer contains an expected string.
8648+
Check whether a certificate issuer contains an expected string.
86498649

86508650
- Remote session: `true`
86518651
- Dry-run support: `true`
@@ -8677,7 +8677,7 @@ with:
86778677

86788678
### `security.pki.cert.key_match_check`
86798679

8680-
Assert that a certificate public key matches a private key.
8680+
Check whether a certificate public key matches a private key.
86818681

86828682
- Remote session: `true`
86838683
- Dry-run support: `true`
@@ -8709,7 +8709,7 @@ with:
87098709

87108710
### `security.pki.cert.san_check`
87118711

8712-
Assert that a certificate contains required Subject Alternative Names.
8712+
Check whether a certificate contains required Subject Alternative Names.
87138713

87148714
- Remote session: `true`
87158715
- Dry-run support: `true`
@@ -8779,7 +8779,7 @@ with:
87798779

87808780
### `security.pki.cert.subject_check`
87818781

8782-
Assert that a certificate subject contains an expected string.
8782+
Check whether a certificate subject contains an expected string.
87838783

87848784
- Remote session: `true`
87858785
- Dry-run support: `true`
@@ -8959,7 +8959,7 @@ with:
89598959

89608960
### `security.secret.redact_check`
89618961

8962-
Assert that a payload contains no declared secret values after redaction policy is applied.
8962+
Check whether a payload contains no declared secret values after redaction policy is applied.
89638963

89648964
- Remote session: `false`
89658965
- Dry-run support: `true`

src/automax/plugins/cert_ops.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from automax.core.models import ExecutionContext, PluginResult
1111
from automax.plugins.base import BasePlugin
12-
from automax.plugins.remote_utils import CHANGE_MARKER, exec_remote, quote, result_from_remote, sudo_prefix
12+
from automax.plugins.remote_utils import CHANGE_MARKER, exec_remote, predicate_result_from_remote, quote, result_from_remote, sudo_prefix
1313

1414

1515

@@ -70,9 +70,15 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
7070

7171
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
7272
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
73-
if rc != 0:
74-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.pki.cert.chain_check failed")
75-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err)
73+
return predicate_result_from_remote(
74+
rc=rc,
75+
stdout=out,
76+
stderr=err,
77+
message="security.pki.cert.chain_check failed",
78+
data_key="valid",
79+
data={"cert": str(params["cert"]), "ca_file": str(params["ca_file"])},
80+
false_rcs=(1, 2),
81+
)
7682

7783
class CertInstallKeypairPlugin(BasePlugin):
7884
name = "security.pki.cert.install_keypair"
@@ -157,14 +163,14 @@ def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginRe
157163

158164
class CertMatchesKeyPlugin(BasePlugin):
159165
name = "security.pki.cert.key_match_check"
160-
description = "Assert that a certificate public key matches a private key."
166+
description = "Check whether a certificate public key matches a private key."
161167
required_params = ("cert", "key")
162168
optional_params = ("sudo",)
163169
opens_remote_session = True
164170
supports_check_mode = True
165171

166172
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
167-
return "security.pki.cert.key_match_check is a read-only certificate/private-key assertion"
173+
return "security.pki.cert.key_match_check is a read-only certificate/private-key predicate"
168174

169175
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
170176
sudo = sudo_prefix(params, default=True)
@@ -174,21 +180,27 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
174180

175181
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
176182
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
177-
if rc != 0:
178-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.pki.cert.key_match_check failed")
179-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err)
183+
return predicate_result_from_remote(
184+
rc=rc,
185+
stdout=out,
186+
stderr=err,
187+
message="security.pki.cert.key_match_check failed",
188+
data_key="matches",
189+
data={"cert": str(params["cert"]), "key": str(params["key"])},
190+
false_rcs=(1, 2),
191+
)
180192

181193

182194
class CertSanAssertPlugin(BasePlugin):
183195
name = "security.pki.cert.san_check"
184-
description = "Assert that a certificate contains required Subject Alternative Names."
196+
description = "Check whether a certificate contains required Subject Alternative Names."
185197
required_params = ("cert", "names")
186198
optional_params = ("sudo",)
187199
opens_remote_session = True
188200
supports_check_mode = True
189201

190202
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
191-
return "security.pki.cert.san_check is a read-only certificate SAN assertion"
203+
return "security.pki.cert.san_check is a read-only certificate SAN predicate"
192204

193205
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
194206
self.validate(params)
@@ -201,51 +213,69 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
201213

202214
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
203215
rc, out, err = exec_remote(context, " && ".join(self.manual_commands(params, context)))
204-
if rc != 0:
205-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.pki.cert.san_check failed")
206-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err)
216+
return predicate_result_from_remote(
217+
rc=rc,
218+
stdout=out,
219+
stderr=err,
220+
message="security.pki.cert.san_check failed",
221+
data_key="matches",
222+
data={"cert": str(params["cert"])},
223+
false_rcs=(1, 2),
224+
)
207225

208226

209227
class CertSubjectAssertPlugin(BasePlugin):
210228
name = "security.pki.cert.subject_check"
211-
description = "Assert that a certificate subject contains an expected string."
229+
description = "Check whether a certificate subject contains an expected string."
212230
required_params = ("cert", "subject")
213231
optional_params = ("sudo",)
214232
opens_remote_session = True
215233
supports_check_mode = True
216234

217235
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
218-
return "security.pki.cert.subject_check is a read-only certificate subject assertion"
236+
return "security.pki.cert.subject_check is a read-only certificate subject predicate"
219237

220238
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
221239
return [f"{sudo_prefix(params, default=True)}openssl x509 -in {quote(params['cert'])} -noout -subject | grep -F -- {quote(params['subject'])}"]
222240

223241
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
224242
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
225-
if rc != 0:
226-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.pki.cert.subject_check failed")
227-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err)
243+
return predicate_result_from_remote(
244+
rc=rc,
245+
stdout=out,
246+
stderr=err,
247+
message="security.pki.cert.subject_check failed",
248+
data_key="matches",
249+
data={"cert": str(params["cert"]), "subject": str(params["subject"])},
250+
false_rcs=(1, 2),
251+
)
228252

229253

230254
class CertIssuerAssertPlugin(BasePlugin):
231255
name = "security.pki.cert.issuer_check"
232-
description = "Assert that a certificate issuer contains an expected string."
256+
description = "Check whether a certificate issuer contains an expected string."
233257
required_params = ("cert", "issuer")
234258
optional_params = ("sudo",)
235259
opens_remote_session = True
236260
supports_check_mode = True
237261

238262
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
239-
return "security.pki.cert.issuer_check is a read-only certificate issuer assertion"
263+
return "security.pki.cert.issuer_check is a read-only certificate issuer predicate"
240264

241265
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
242266
return [f"{sudo_prefix(params, default=True)}openssl x509 -in {quote(params['cert'])} -noout -issuer | grep -F -- {quote(params['issuer'])}"]
243267

244268
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
245269
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
246-
if rc != 0:
247-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.pki.cert.issuer_check failed")
248-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err)
270+
return predicate_result_from_remote(
271+
rc=rc,
272+
stdout=out,
273+
stderr=err,
274+
message="security.pki.cert.issuer_check failed",
275+
data_key="matches",
276+
data={"cert": str(params["cert"]), "issuer": str(params["issuer"])},
277+
false_rcs=(1, 2),
278+
)
249279

250280

251281
class CertInstallCaBundlePlugin(BasePlugin):

src/automax/plugins/fs_system.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,14 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
130130
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
131131
command = " && ".join(self.manual_commands(params, context))
132132
rc, out, err = exec_remote(context, command)
133-
if rc != 0:
134-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="fs.attr.check failed")
135-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err, data={"path": params["path"], "attrs": _attr_entries(params["attrs"])})
133+
return predicate_result_from_remote(
134+
rc=rc,
135+
stdout=out,
136+
stderr=err,
137+
message="fs.attr.check failed",
138+
data_key="matches",
139+
data={"path": params["path"], "attrs": _attr_entries(params["attrs"])},
140+
)
136141

137142

138143
class FsQuotaPlugin(BasePlugin):
@@ -219,9 +224,14 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
219224
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
220225
command = " && ".join(self.manual_commands(params, context))
221226
rc, out, err = exec_remote(context, command)
222-
if rc != 0:
223-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="fs.acl.check failed")
224-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err, data={"path": params["path"], "acl": _acl_entries(params["acl"])})
227+
return predicate_result_from_remote(
228+
rc=rc,
229+
stdout=out,
230+
stderr=err,
231+
message="fs.acl.check failed",
232+
data_key="matches",
233+
data={"path": params["path"], "acl": _acl_entries(params["acl"])},
234+
)
225235

226236

227237
class FsAclRestorePlugin(BasePlugin):

src/automax/plugins/pam_ops.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from automax.core.models import ExecutionContext, PluginResult
1212
from automax.plugins.base import BasePlugin, PluginValidationError
13-
from automax.plugins.remote_utils import cleanup_trap_command, CHANGE_MARKER, exec_remote, heredoc_to_file_expr, heredoc_to_stdin, shell_var_ref, tempfile_command, quote, result_from_remote, sudo_prefix
13+
from automax.plugins.remote_utils import cleanup_trap_command, CHANGE_MARKER, exec_remote, heredoc_to_file_expr, heredoc_to_stdin, predicate_result_from_remote, shell_var_ref, tempfile_command, quote, result_from_remote, sudo_prefix
1414

1515

1616

@@ -362,7 +362,7 @@ def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginRe
362362

363363
class PamAuthselectPlugin(BasePlugin):
364364
name = "security.authselect.check"
365-
description = "Assert the current authselect profile and enabled features on RHEL-like systems."
365+
description = "Check the current authselect profile and enabled features on RHEL-like systems."
366366
optional_params = ("profile", "features", "sudo")
367367
opens_remote_session = True
368368
supports_check_mode = True
@@ -371,7 +371,7 @@ def _features(self, params: Dict[str, Any]) -> list[str]:
371371
return _as_list(params.get("features"))
372372

373373
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
374-
return "security.authselect.check is a read-only authselect profile assertion"
374+
return "security.authselect.check is a read-only authselect profile predicate"
375375

376376
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
377377
command = f"{sudo_prefix(params, default=True)}authselect current"
@@ -383,6 +383,11 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
383383

384384
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
385385
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
386-
if rc != 0:
387-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="security.authselect.check failed")
388-
return PluginResult.success(changed=False, rc=rc, stdout=out, stderr=err, data={"authselect": out})
386+
return predicate_result_from_remote(
387+
rc=rc,
388+
stdout=out,
389+
stderr=err,
390+
message="security.authselect.check failed",
391+
data_key="matches",
392+
data={"authselect": out},
393+
)

src/automax/plugins/pki.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from automax.core.models import ExecutionContext, PluginResult
1313
from automax.plugins.base import BasePlugin, PluginValidationError
14-
from automax.plugins.remote_utils import cleanup_trap_command, CHANGE_MARKER, exec_remote, heredoc_to_file_expr, shell_var_ref, tempfile_command, quote, result_from_remote, sudo_prefix
14+
from automax.plugins.remote_utils import cleanup_trap_command, CHANGE_MARKER, exec_remote, heredoc_to_file_expr, predicate_result_from_remote, shell_var_ref, tempfile_command, quote, result_from_remote, sudo_prefix
1515

1616

1717

@@ -118,13 +118,13 @@ def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginRe
118118

119119
class PkiCertExpiryAssertPlugin(BasePlugin):
120120
name = "security.pki.cert.expiry_check"
121-
description = "Assert that a certificate remains valid for at least min_days."
121+
description = "Check whether a certificate remains valid for at least min_days."
122122
required_params = ("path",)
123123
optional_params = ("min_days", "sudo")
124124
opens_remote_session = True
125125

126126
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
127-
return "security.pki.cert.expiry_check is a read-only certificate assertion with no file diff"
127+
return "security.pki.cert.expiry_check is a read-only certificate expiry predicate with no file diff"
128128

129129
def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) -> list[str]:
130130
self.validate(params)
@@ -133,6 +133,12 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
133133

134134
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
135135
rc, out, err = exec_remote(context, self.manual_commands(params, context)[0])
136-
if rc != 0:
137-
return PluginResult.failure(rc=rc, stdout=out, stderr=err, message="certificate expiry assertion failed")
138-
return PluginResult.success(stdout=out, stderr=err, data={"path": params["path"], "min_days": int(params.get("min_days", 30))})
136+
return predicate_result_from_remote(
137+
rc=rc,
138+
stdout=out,
139+
stderr=err,
140+
message="security.pki.cert.expiry_check failed",
141+
data_key="valid",
142+
data={"path": params["path"], "min_days": int(params.get("min_days", 30))},
143+
false_rcs=(1, 2),
144+
)

src/automax/plugins/redaction.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (C) 2026 Marco Fortina
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

4-
"""Secret redaction policy assertion plugins."""
4+
"""Secret redaction policy check plugins."""
55

66
from __future__ import annotations
77

@@ -24,7 +24,7 @@ def _payload(params: Dict[str, Any], context: ExecutionContext) -> str:
2424

2525
class SecretRedactAssertPlugin(BasePlugin):
2626
name = "security.secret.redact_check"
27-
description = "Assert that a payload contains no declared secret values after redaction policy is applied."
27+
description = "Check whether a payload contains no declared secret values after redaction policy is applied."
2828
optional_params = ("text", "value", "source")
2929
supports_check_mode = True
3030

@@ -38,14 +38,19 @@ def manual_commands(self, params: Dict[str, Any], context: ExecutionContext) ->
3838
return ["# security.secret.redact_check is evaluated inside Automax before output persistence"]
3939

4040
def diff_preview_reason(self, params: Dict[str, Any], context: ExecutionContext) -> str:
41-
return "security.secret.redact_check is an in-process redaction policy assertion"
41+
return "security.secret.redact_check is an in-process redaction policy predicate"
4242

4343
def execute(self, params: Dict[str, Any], context: ExecutionContext) -> PluginResult:
4444
text = _payload(params, context)
4545
leaked = find_unredacted_secrets(text, context.secrets)
46-
if leaked:
47-
return PluginResult.failure(message="unredacted secret values detected", data={"leaks": len(leaked)})
48-
return PluginResult.success(changed=False, data={"redacted": redact_text(text, context.secrets)})
46+
return PluginResult.success(
47+
changed=False,
48+
data={
49+
"clean": not leaked,
50+
"leaks": len(leaked),
51+
"redacted": redact_text(text, context.secrets),
52+
},
53+
)
4954

5055

5156
class SecretScanOutputPlugin(BasePlugin):

0 commit comments

Comments
 (0)