NERC CIP Compliance Logging

An auditor pulls ninety days of security-event logs from your settlement platform and finds a four-hour gap the night a resettlement batch ran with elevated privileges — no logon record, no configuration-change entry, no way to prove the ledger written that night was not silently altered. That gap is a NERC CIP finding, and it is exactly the failure this section is built to prevent. Within the Regulatory Filing Preparation framework, compliance logging is the control that makes every other filing defensible: an Electric Quarterly Report or a state reconciliation is only as trustworthy as the audit trail proving who touched the settlement engine, what they changed, and that no one rewrote the record afterward. This guide maps the NERC Critical Infrastructure Protection (CIP) standards family onto the concrete logging your settlement and Energy Trading and Risk Management (ETRM) systems must emit, and hands the operational controls — event capture, retention, tamper evidence, and access-log review — to Python automation teams who own those systems.

The systems in scope are the ones that produce settlement-grade numbers: the calculation engines described in Settlement Calculation & Validation Engines, the ingestion tier in ETRM System Architecture, and the boundary controls in Security & Access Boundaries. Where those pages describe the data flow, this one describes the parallel evidence stream that flows alongside it. The diagram below traces that logging pipeline, from raw event emission through enrichment and hash-chaining into an append-only store that is continuously reviewed and alerted on.

NERC CIP compliance logging pipeline for settlement systems Security events emitted by settlement and ETRM systems are captured and enriched with actor, asset, and CIP-requirement tags, then hash-chained and signed for tamper evidence, then written to an append-only store with a ninety-day online retention window. A continuous review and alerting stage reads the store and routes anomalies to the exception workflow. Event sourcesettlement / ETRM Capture & enrichactor · asset · CIP tag Hash-chain & signtamper evidence Append-only store90-day online Review & alertaccess-log sweep CIP-007 R4.4 review

Two properties separate a CIP-defensible log from an ordinary application log. First, completeness: the log must capture every event the standard enumerates, without gaps, across every asset in the electronic security perimeter — a missing event is treated as a control failure, not a convenience. Second, integrity: the log itself must be tamper-evident, so an auditor can prove the record has not been edited after the fact. The concrete cryptographic implementation of that second property — a hash-chained, append-only log with chain verification — is worked end to end in Building tamper-evident audit logs for NERC CIP. This page frames the standards, the event catalogue, the retention math, and the review cadence that implementation has to satisfy.

Specification & Standards

NERC CIP is a family of mandatory, enforceable reliability standards approved by FERC and applied to Bulk Electric System (BES) Cyber Systems. Not every settlement server is automatically in scope — scope is driven by the CIP-002 asset categorization, which rates each Cyber System as high, medium, or low impact based on the facilities it supports. A settlement engine that receives meter and market data from, or issues instructions to, a medium- or high-impact control environment inherits the logging obligations of that rating. Even where a settlement system sits outside the formal BES perimeter, most utilities apply the same controls voluntarily because the audit trail doubles as the evidence base for FERC filings and counterparty dispute resolution.

The standards most relevant to logging are CIP-007, CIP-010, and CIP-011, with CIP-004 and CIP-005 governing the access events that must be logged. The table below maps each requirement to the concrete logging obligation it imposes on a settlement or ETRM system. Treat it as the acceptance criteria for the capture stage: every row is an event class your emitter must produce and your store must retain.

CIP requirement What it governs Logging obligation for the settlement / ETRM system
CIP-007 R4.1 Security event monitoring — event types to log Log detected successful and failed login attempts, malicious-code detection, and access-control changes on every in-scope asset
CIP-007 R4.2 Alerting on security events Generate alerts for events the entity deems reportable (e.g. repeated auth failure, privilege escalation)
CIP-007 R4.3 Log retention Retain logged events at least 90 calendar days, except during a CIP Exceptional Circumstance
CIP-007 R4.4 Log review Review a summarization or sampling of logged events at least every 15 calendar days for anomalies
CIP-010 R1 Configuration baselines Log the authorized baseline and every change to OS, software, and settlement code/config against it
CIP-010 R2.1 Change monitoring Detect and log deviations from the baseline at least every 35 calendar days; investigate unauthorized changes
CIP-011 R1 Information protection (BCSI) Identify, and protect in logs, BES Cyber System Information — redact or encrypt sensitive fields, control log access
CIP-004 R4/R5 Access management Log every grant, modification, and revocation of access to the settlement system and its data
CIP-005 R1/R2 Electronic security perimeter Log inbound and outbound access at the perimeter and all interactive remote access sessions

Three obligations from this table dominate the design. CIP-007 R4 is the backbone: it fixes what security events are captured, that they must be retained for at least 90 days, and that a human must review them on a 15-day cadence. CIP-010 turns the log into a change record — every modification to the settlement code, the tariff tables, or the runtime configuration must be traceable to an authorized change against a known baseline, so an unexplained code change surfaces as a monitored deviation rather than a silent drift. CIP-011 constrains the log’s own contents: a settlement audit trail frequently embeds BES Cyber System Information (node identifiers, network paths, credential references), and that information must be protected inside the log the same way it is protected in the source system. The retention floor is worth stating precisely, since it drives storage sizing:

$$R_{online} \geq 90 \text{ days}, \qquad S \approx r_{events/day} \times b_{bytes/event} \times R_{online}$$

where an in-scope fleet emitting tens of thousands of events per day accumulates into gigabytes of online, queryable log that must remain both available and provably intact for the full window.

Step-by-Step Implementation

The logging subsystem is built in four production stages: define a canonical event schema, emit enriched events from the settlement code, protect sensitive fields per CIP-011, and record configuration changes per CIP-010. Each stage below is a small, testable unit. The cryptographic chaining that makes the store tamper-evident is deliberately left to Building tamper-evident audit logs for NERC CIP; here the focus is producing correct, complete, well-tagged events for that store to consume.

Step 1 — Define a canonical CIP event schema. Every event, whatever its source, is normalized to one structure so the review and retention logic is uniform. The schema carries the actor, the asset, the CIP requirement it satisfies, and a UTC timestamp. Modeling it explicitly (here with a dataclass; a Pydantic model works identically) lets the capture boundary reject a malformed event instead of writing a partial record.

from dataclasses import dataclass, asdict, field
from datetime import datetime, timezone
from decimal import Decimal
from typing import Optional
import json

@dataclass(frozen=True)
class CipSecurityEvent:
    """One normalized CIP-loggable security event from a settlement/ETRM asset."""
    event_time: datetime          # timezone-aware UTC; naive timestamps are rejected
    asset_id: str                 # in-scope Cyber System, e.g. "settle-engine-01"
    actor: str                    # authenticated principal or service account
    action: str                   # LOGIN_SUCCESS, LOGIN_FAILURE, CONFIG_CHANGE, ...
    cip_requirement: str          # "CIP-007-R4.1", "CIP-010-R1", ...
    outcome: str                  # "success" | "failure" | "detected"
    settlement_run_id: Optional[str] = None   # ledger run this event touched, if any
    detail: dict = field(default_factory=dict)

    def to_canonical_json(self) -> str:
        """Deterministic serialization — the exact bytes that will be hash-chained."""
        if self.event_time.tzinfo is None:
            raise ValueError(f"event_time must be tz-aware: {self.event_time!r}")
        payload = asdict(self)
        payload["event_time"] = self.event_time.astimezone(timezone.utc).isoformat()
        # sort_keys makes the serialization reproducible for verification.
        return json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)

Step 2 — Emit enriched events from the settlement code. The settlement engine calls a single logger at every security-relevant boundary: authentication, a run kicked off, a tariff table swapped, a ledger written. Enrichment attaches the CIP requirement tag at the call site, because only the code that raised the event knows which control it evidences. Keeping money values as Decimal here matters even in the log, so a later dispute can reconcile the logged figure to the ledger to the cent.

import logging

audit_logger = logging.getLogger("cip.audit")

def emit_settlement_event(
    asset_id: str,
    actor: str,
    action: str,
    cip_requirement: str,
    outcome: str = "success",
    settlement_run_id: Optional[str] = None,
    charge_amount: Optional[Decimal] = None,
) -> CipSecurityEvent:
    """Build, log, and hand off one CIP event. Returns it for the chaining layer."""
    detail = {}
    if charge_amount is not None:
        # Persist money as an exact decimal string, never a binary float.
        detail["charge_amount"] = str(charge_amount.quantize(Decimal("0.01")))
    event = CipSecurityEvent(
        event_time=datetime.now(timezone.utc),
        asset_id=asset_id,
        actor=actor,
        action=action,
        cip_requirement=cip_requirement,
        outcome=outcome,
        settlement_run_id=settlement_run_id,
        detail=detail,
    )
    audit_logger.info(event.to_canonical_json())
    return event  # the append-only chain writer consumes this next

Step 3 — Protect BES Cyber System Information (CIP-011). Before an event is persisted or shipped to a review console, sensitive fields are redacted or tokenized so the log does not itself become an uncontrolled disclosure of BCSI. Redaction is applied to a copy on the way out; the protected value can still be recovered through a controlled, separately-logged path if an investigation requires it.

import hashlib

BCSI_FIELDS = {"ip_address", "hostname", "credential_ref", "circuit_id"}

def redact_bcsi(event: CipSecurityEvent, salt: bytes) -> dict:
    """Return a CIP-011-safe view: BCSI fields replaced by a salted token."""
    safe = json.loads(event.to_canonical_json())
    for key, value in list(safe.get("detail", {}).items()):
        if key in BCSI_FIELDS and value is not None:
            token = hashlib.sha256(salt + str(value).encode()).hexdigest()[:16]
            safe["detail"][key] = f"bcsi:{token}"   # consistent token, no plaintext
    return safe

Step 4 — Record configuration changes against a baseline (CIP-010). Every change to the settlement runtime — a new code revision, an updated tariff table, a modified tolerance band — is logged as a CONFIG_CHANGE event carrying the baseline hash before and after. The periodic monitoring job then compares the live configuration hash to the last authorized baseline and raises a deviation if they differ without an intervening authorized change.

def hash_baseline(config_files: dict[str, bytes]) -> str:
    """Deterministic hash of the settlement runtime's configuration baseline."""
    digest = hashlib.sha256()
    for name in sorted(config_files):                 # sorted → order-independent
        digest.update(name.encode())
        digest.update(config_files[name])
    return digest.hexdigest()

def detect_config_deviation(live: dict[str, bytes], authorized_baseline: str,
                            asset_id: str, actor: str = "cip010-monitor") -> Optional[CipSecurityEvent]:
    """CIP-010 R2.1: flag a live config that drifted from the authorized baseline."""
    live_hash = hash_baseline(live)
    if live_hash == authorized_baseline:
        return None
    return emit_settlement_event(
        asset_id=asset_id, actor=actor, action="CONFIG_DEVIATION",
        cip_requirement="CIP-010-R2.1", outcome="detected",
    )

Edge Cases & Failure Modes

Compliance logging fails in quiet, systematic ways that only surface under audit. The recurring hazards below each have a defined handling posture; the table then summarizes the CIP consequence of getting each one wrong.

Clock skew across assets. Events from different servers are correlated by timestamp, so an unsynchronized clock scrambles the sequence and can hide an intrusion. Every in-scope asset must be NTP-synchronized, timestamps recorded in UTC, and a clock-drift check itself logged as a security event.

Log-source outage. If an asset stops shipping events, the absence must itself raise an alert — silence is indistinguishable from a clean period otherwise. A heartbeat event per asset lets the review job detect a gap rather than trust an empty result.

DST and timezone collapse. Settlement runs key off a market clock that observes daylight-saving transitions, but CIP logs must be stored in UTC to stay monotonic across the spring-forward gap and fall-back overlap. Storing local time risks two events sharing a wall-clock label during the repeated autumn hour.

Retention boundary rollover. Deleting logs the instant they turn 91 days old can destroy evidence that is still needed for an open investigation. Retention past 90 days is bounded but must never truncate an event tied to an unresolved incident.

BCSI leakage into free-text fields. An operator note pasted into a detail field can smuggle BCSI past the structured redactor. Free-text fields are the highest-risk CIP-011 surface and must be scanned, not just the enumerated columns.

Failure mode Trigger CIP consequence if unhandled Handling posture
Clock skew Unsynchronized asset clock Event sequence unprovable NTP sync, UTC storage, drift event logged
Log-source outage Agent or asset stops emitting Undetected gap reads as clean Per-asset heartbeat + gap alert
DST overlap Local-time storage in fall-back hour Duplicate wall-clock labels Store UTC, render market-local only on read
Premature purge Retention job deletes at 90 days Evidence for open incident lost Hold on unresolved-incident tag
Free-text BCSI Operator note in detail field CIP-011 disclosure finding Scan free text, not only known fields

Thresholds & Alerting

CIP-007 R4.2 requires alerting on the security events an entity deems reportable, and R4.4 requires a documented review of logged events at least every 15 days. In practice teams run both a continuous alert tier and a scheduled review sweep. Alert thresholds are configuration, not hard-coded logic, so they can be tuned per asset and per season without a code change — the same discipline applied to settlement variance in Threshold Tuning & Alerts. A workable default tiering: authentication failures alert at 5 within 10 minutes for a single account (possible brute force); any privilege escalation or CONFIG_CHANGE outside a change window alerts immediately; a detected CONFIG_DEVIATION from CIP-010 monitoring is a review-required event; and a log-source heartbeat gap exceeding one interval is an operational alert because a blind spot in the log is itself a compliance risk. Escalation routes by tier — informational events to the daily digest, review-required events to the compliance analyst queue, and immediate alerts to the on-call security channel — mirroring the severity routing settlement operations already run.

Testing & Reconciliation

A compliance log is only trustworthy if its completeness and integrity are themselves tested. The shadow approach that works for settlement math works here too: run an independent counter of expected events beside the emitter and reconcile the two. If the settlement engine executed 1,200 runs today, the log must contain 1,200 RUN_START events; a shortfall is a capture bug, not a quiet day. Unit tests assert the schema rejects a naive timestamp, the redactor tokenizes every BCSI field consistently, and the baseline hash is order-independent.

def test_naive_timestamp_rejected():
    from datetime import datetime
    bad = CipSecurityEvent(
        event_time=datetime(2026, 7, 17, 12, 0, 0),   # no tzinfo
        asset_id="settle-engine-01", actor="svc-batch",
        action="RUN_START", cip_requirement="CIP-007-R4.1", outcome="success",
    )
    try:
        bad.to_canonical_json()
        assert False, "naive timestamp should have been rejected"
    except ValueError:
        pass  # expected — capture boundary refuses an unprovable timestamp

def test_bcsi_redaction_is_consistent():
    ev = emit_settlement_event(
        "settle-engine-01", "analyst.jones", "LOGIN_SUCCESS", "CIP-007-R4.1",
    )
    object.__setattr__(ev, "detail", {"ip_address": "10.4.2.9"})
    salt = b"rotating-daily-salt"
    a = redact_bcsi(ev, salt)["detail"]["ip_address"]
    b = redact_bcsi(ev, salt)["detail"]["ip_address"]
    assert a == b and a.startswith("bcsi:")   # same input → same token, no plaintext

Reconciliation against the tamper-evident store closes the loop: periodically re-verify the full hash chain and confirm the event count in the chain matches the emitter’s shadow counter for the window. That verification routine, and the chain construction it checks, are implemented in Building tamper-evident audit logs for NERC CIP.

Frequently Asked Questions

Which settlement systems actually fall under NERC CIP logging?

Scope follows the CIP-002 impact rating of the Cyber Systems involved, not the label “settlement.” A settlement or ETRM system that exchanges data with, or can influence, a medium- or high-impact BES Cyber System inherits CIP-007, CIP-010, and CIP-011 logging obligations for the assets in that perimeter. Many utilities apply the same controls to settlement platforms that sit just outside the formal perimeter, because the audit trail is also the evidence base for FERC filings and counterparty disputes, so the marginal cost of full CIP logging is low relative to its reuse.

How long must settlement security logs be retained, and where?

CIP-007 R4.3 sets a floor of 90 calendar days of retained logged events, available except during a documented CIP Exceptional Circumstance. In practice the 90 days must be kept online and queryable so the R4.4 review can run, while longer archival retention is driven by the entity’s own evidence-retention policy and any open investigations. Never purge exactly at the 90-day boundary if an event is tied to an unresolved incident — the retention job should honor a hold on any event carrying an open-incident tag.

What makes a settlement log tamper-evident rather than just append-only?

Append-only means new records are added and existing ones are not edited in place, but that alone does not prove nobody rewrote the file. Tamper evidence comes from chaining each record to a cryptographic hash of the one before it, so altering any past event breaks every hash after it and the break is detectable by re-walking the chain. The full construction — a linked SHA-256 chain over the canonical event bytes, plus a verification routine — is implemented in the companion guide on building tamper-evident audit logs for NERC CIP.

How do CIP-011 requirements change what a settlement log may contain?

CIP-011 requires that BES Cyber System Information be identified and protected wherever it lives, and a settlement audit trail frequently embeds it — node identifiers, network paths, hostnames, credential references. Those fields must be redacted or tokenized in the log, and access to the log itself controlled, so the audit trail does not become an uncontrolled disclosure. The practical control is a redaction pass over both the enumerated sensitive fields and any free-text notes before an event leaves the trust boundary for a review console or an external filing.

Explore this topic