Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
**IMA-PCR-Utils** (`imapcrutils`) is a Python library for Integrity Measurement
Architecture (IMA) and Platform Configuration Register (PCR), providing
functionality for parsing IMA log entries, calculating PCR10 hash values and
boot_aggregate values.
boot_aggregate values, and appraising IMA log entries against a YAML allow/deny
policy.

## Installation

Expand Down Expand Up @@ -37,6 +38,13 @@ The `imapcrutils` module consists of the following public types and functions:
| `truncate_ima_log_by_pcr` | Truncate the IMA log at the point where the calculated PCR matches the reference value. |
| `validate_ima_log_entry` | Validate a single entry by comparing the template hash with the recomputed value. |
| `calculate_boot_aggregate` | Calculate `boot_aggregate` from PCR0..PCR9 values. |
| `AppraisalResult` | Verdict (`ALLOW` / `DENY` / `NEUTRAL`) for a single IMA log entry against an appraisal policy. |
| `PolicyComponent` | A single named component of an appraisal policy (`name`, `path` glob, optional `allow`/`deny` hash sets). |
| `AppraisalPolicy` | An ordered collection of `PolicyComponent`s; the first matching component decides the verdict for an entry. |
| `load_policy` | Parse a YAML appraisal policy string into an `AppraisalPolicy`. |
| `load_policy_file` | Load an `AppraisalPolicy` from a YAML file on disk. |
| `appraise_ima_log` | Classify each IMA log entry against an appraisal policy, returning `(entry, verdict)` pairs. |
| `verify_ima_log` | Return `True` when no IMA log entry is denied by the policy. |

### CLI Tools / Example Scripts

Expand All @@ -48,6 +56,7 @@ and command-line tools. Sample IMA log and PCR list files are also available.
| `pcr10.py` | Calculate PCR10 from input IMA log |
| `truncate_log.py` | Truncate IMA log at the point where the calculated PCR matches the reference value |
| `boot_aggregate.py` | Calculate boot_aggregate from PCR list file including PCR[0-9] |
| `appraise.py` | Appraise IMA log entries against a YAML allow/deny policy |

### Compare with the true PCR10 hash value

Expand Down
51 changes: 47 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ where vTPM is available.
python pcr10.py

# Replay PCR10 from the sample IMA log file
python pcr10.py -i ascii_runtime_measurements
python pcr10.py -i sample_input/ascii_runtime_measurements
```

## truncate_log
Expand Down Expand Up @@ -81,14 +81,57 @@ the matching entry.

```shell
# Truncate IMA log using reference PCR10 from pcrlist_2.bin
python truncate_pcr10.py -i ascii_runtime_measurements_2 -p c5bfcd40187bfc190fe9c584b8b2675f08180c0e9579255fa9eba91e7d18f678
python truncate_pcr10.py -i sample_input/ascii_runtime_measurements_2 -p c5bfcd40187bfc190fe9c584b8b2675f08180c0e9579255fa9eba91e7d18f678

# Save truncated log to file
python truncate_pcr10.py -i ascii_runtime_measurements_2 \
python truncate_pcr10.py -i sample_input/ascii_runtime_measurements_2 \
-p c5bfcd40187bfc190fe9c584b8b2675f08180c0e9579255fa9eba91e7d18f678 \
-o truncated_measurements.txt
```

## appraise

### Usage

```shell
python appraise.py [-i $IMA_LOG_PATH] -p $POLICY_PATH [-s $SHOW] [-o $OUTPUT_PATH]
```

```shell
python appraise.py [--in $IMA_LOG_PATH] --policy $POLICY_PATH [--show $SHOW] [--out $OUTPUT_PATH]
```

### Required Arguments

- `-p, --policy`: Path to the YAML appraisal policy file

### Options

- `-i, --in`: Path to the IMA log file (default: `/sys/kernel/security/ima/ascii_runtime_measurements`)
- `-s, --show`: Which verdicts to print: `all`, `deny`, `non-allow` (deny +
neutral) (default: `deny`)
- `-o, --out`: Path to the output file (default: stdout)

### Description

This tool classifies each IMA log entry against a YAML appraisal policy and
prints the selected verdicts as tab-separated `<verdict>\t<hash>\t<path>` lines.
A summary is written to stderr. Exit status is `0` when no entry is denied,
`1` when at least one entry is denied, and `2` on I/O or policy parse errors.

See [appraise_policy.yaml](appraise_policy.yaml) for the policy format.

### Example

```shell
# Print all denied entries (default) for the sample IMA log
python appraise.py -i sample_input/ascii_runtime_measurements -p sample_input/appraise_policy.yaml

# Show every verdict
python appraise.py -i sample_input/ascii_runtime_measurements \
-p appraise_policy.yaml -s all
```

## boot-aggregate

### Usage
Expand Down Expand Up @@ -121,5 +164,5 @@ python boot_aggregate.py --in $PCR_LIST_PATH --selector $SELECTOR [--hash-algori

```shell
# Calculate boot_aggregate from the sample PCR list file
python boot_aggregate.py --in pcr_list.bin -s sha256:0,1,2,3,4,5,6,7,8,9,10,12,14,23
python boot_aggregate.py --in sample_input/pcrlist.bin -s sha256:0,1,2,3,4,5,6,7,8,9,10,12,14,23
```
128 changes: 128 additions & 0 deletions examples/appraise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""
CLI tool for appraising IMA log entries against a YAML policy.
"""

import argparse
import sys

from imapcrutils import AppraisalResult, appraise_ima_log, load_policy_file, parse_ima_log_string

DEFAULT_IMA_LOG_PATH = "/sys/kernel/security/ima/ascii_runtime_measurements"

SHOW_CHOICES = ["all", "deny", "non-allow"]


def filter_results(results, show: str):
"""
Filter (entry, verdict) pairs by --show mode.
"""
match show:
case "all":
return results
case "deny":
return [(e, v) for e, v in results if v is AppraisalResult.DENY]
case "non-allow":
return [(e, v) for e, v in results if v is not AppraisalResult.ALLOW]
case _:
# will not reach here (choices enforced by argparse)
raise ValueError(f"Invalid show mode: {show}")


def format_results(results) -> str:
"""
Format (entry, verdict) pairs as tab-separated lines.
"""
lines = [f"{verdict.value}\t{entry.file_hash.hex()}\t{entry.file_path}" for entry, verdict in results]
return "\n".join(lines) + "\n" if lines else ""


def write_output(text: str, output_path: str | None) -> None:
"""
Write text to file or stdout.
"""
if output_path is None:
print(text, end="")
else:
with open(output_path, "w", encoding="utf-8") as f:
f.write(text)


def main() -> int:
"""
Main function.
"""
parser = argparse.ArgumentParser(description="Appraise IMA log entries against a YAML policy.")
parser.add_argument(
"-i",
"--in",
dest="input_path",
default=DEFAULT_IMA_LOG_PATH,
help=f"Path to the IMA log file (default: {DEFAULT_IMA_LOG_PATH})",
)
parser.add_argument(
"-p",
"--policy",
dest="policy_path",
required=True,
help="Path to the YAML appraisal policy file",
)
parser.add_argument(
"-s",
"--show",
dest="show",
type=str.lower,
default="deny",
choices=SHOW_CHOICES,
help="Which verdicts to print: all entries, deny only, or non-allow (deny + neutral) (default: deny)",
)
parser.add_argument(
"-o",
"--out",
dest="output_path",
default=None,
help="Path to the output file (default: stdout)",
)

args = parser.parse_args()

# Load the appraisal policy
try:
policy = load_policy_file(args.policy_path)
except FileNotFoundError:
print(f"Error: policy file not found: {args.policy_path}", file=sys.stderr)
return 2
except ValueError as e:
print(f"Error: invalid policy: {e}", file=sys.stderr)
return 2

# Read IMA log entries
try:
with open(args.input_path, encoding="utf-8") as f:
lines = f.read()
except FileNotFoundError:
print(f"Error: IMA log file not found: {args.input_path}", file=sys.stderr)
return 2
except OSError as e:
print(f"Error reading IMA log file: {e}", file=sys.stderr)
return 2

entries = parse_ima_log_string(lines)
results = appraise_ima_log(entries, policy)

write_output(format_results(filter_results(results, args.show)), args.output_path)

deny_count = sum(1 for _, v in results if v is AppraisalResult.DENY)
allow_count = sum(1 for _, v in results if v is AppraisalResult.ALLOW)
neutral_count = len(results) - deny_count - allow_count
print(
f"Appraised {len(results)} entries: {allow_count} allow, {deny_count} deny, {neutral_count} neutral",
file=sys.stderr,
)

return 1 if deny_count > 0 else 0


if __name__ == "__main__":
sys.exit(main())
9 changes: 9 additions & 0 deletions examples/sample_input/appraise_policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
boot_aggregate:
path: boot_aggregate
allow: [088faac4777b024045bd578c5c3f8efc4ac2cafb4af90a12832a762feb58eb88]

kernel_modules:
path: "*/**/autofs4.ko.zst"
allow:
- bbdbca85a82cf96aad024bdfedaa4fcc791d857b338b6f7c5a4625bce0118ac4
- cf06a09ff00ee3275779e83cf9a4037dd822ba9dc16442584212f605ba71e341
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions imapcrutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@
truncate_ima_log_by_pcr,
validate_ima_log_entry,
)
from imapcrutils.verify import (
AppraisalPolicy,
AppraisalResult,
PolicyComponent,
appraise_ima_log,
load_policy,
load_policy_file,
verify_ima_log,
)
Loading