# Stale Password Audit

LLMS index: [llms.txt](/llms.txt)

---

<div id="pslens-context-panel" class="card border-info mb-4 d-none">
  <div class="card-header bg-light text-info py-2 fw-bold d-flex align-items-center border-bottom border-info-subtle">
    <i class="bi bi-info-circle-fill me-2"></i>
    <span>Tailored Operational Context</span>
  </div>
  <div class="card-body p-0">
    <ul class="list-group list-group-flush">
      <li id="row-db" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Target Database:</strong>
        <span id="ctx-db" class="badge bg-secondary font-monospace">&mdash;</span>
      </li>
      <li id="row-type" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Context Type:</strong>
        <span id="ctx-type" class="badge bg-light text-dark border font-monospace text-uppercase">&mdash;</span>
      </li>
      <li id="row-severity" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Alert Severity:</strong>
        <span id="ctx-severity" class="badge">&mdash;</span>
      </li>
      <li id="row-time" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Triggered Time:</strong>
        <span id="ctx-time" class="text-muted small">&mdash;</span>
      </li>
      <li id="row-details" class="list-group-item py-2 d-none">
        <strong id="label-details" class="d-block mb-1">Firing Context:</strong>
        <code id="ctx-details" class="d-block p-2 bg-light border rounded small" style="white-space: pre-wrap; word-break: break-all;">&mdash;</code>
      </li>
    </ul>
  </div>
</div>

<script>
  (function() {
    const params = new URLSearchParams(window.location.search);
    const metadata = params.get('metadata');
    if (!metadata) return;

    try {
      
      const base64 = metadata.replace(/-/g, '+').replace(/_/g, '/');
      const jsonStr = decodeURIComponent(escape(window.atob(base64)));
      const data = JSON.parse(jsonStr);

      if (data) {
        let hasData = false;

        if (data.db) {
          document.getElementById('ctx-db').textContent = data.db;
          document.getElementById('row-db').classList.remove('d-none');
          hasData = true;
        }

        if (data.type) {
          document.getElementById('ctx-type').textContent = data.type;
          document.getElementById('row-type').classList.remove('d-none');
          hasData = true;
        }

        if (data.severity) {
          const severityBadge = document.getElementById('ctx-severity');
          const severity = data.severity.toLowerCase();
          severityBadge.textContent = severity.toUpperCase();
          if (severity === 'critical') {
            severityBadge.className = 'badge bg-danger';
          } else if (severity === 'warning') {
            severityBadge.className = 'badge bg-warning text-dark';
          } else {
            severityBadge.className = 'badge bg-info';
          }
          document.getElementById('row-severity').classList.remove('d-none');
          hasData = true;
        }

        if (data.t) {
          const date = new Date(data.t * 1000);
          document.getElementById('ctx-time').textContent = date.toLocaleString();
          document.getElementById('row-time').classList.remove('d-none');
          hasData = true;
        }

        if (data.details) {
          document.getElementById('ctx-details').textContent = data.details;

          
          const labelDetails = document.getElementById('label-details');
          if (data.type === 'object') {
            labelDetails.textContent = 'Object Metadata Details:';
          } else if (data.type === 'report') {
            labelDetails.textContent = 'Report Description:';
          } else {
            labelDetails.textContent = 'Firing Context:';
          }

          document.getElementById('row-details').classList.remove('d-none');
          hasData = true;
        }

        if (hasData) {
          document.getElementById('pslens-context-panel').classList.remove('d-none');
        }
      }
    } catch (e) {
      console.error('Failed to parse operational context metadata:', e);
    }
  })();
</script>


## Stale Password Audit Report

**Report ID:** `security-stale-passwords`
**Category:** Security

## Purpose

This report identifies unlocked PeopleSoft user accounts whose passwords have not been changed within a configurable number of days. External auditors will ask. SSO accounts are automatically excluded, so the list is users who still have a real PeopleSoft password.

## What It Detects

The report categorizes stale password accounts into three severity levels based on how long the password has been unchanged:

### CRITICAL — Password Not Changed in Over 1 Year

Unlocked accounts where the password has not been changed in over 365 days. These represent the highest risk and should be addressed immediately.

### WARNING — Password Not Changed in Over 180 Days

Unlocked accounts where the password is between 180 and 365 days old.

### INFO — Password Exceeds Configured Threshold

Unlocked accounts where the password exceeds the configured threshold (default 90 days) but is less than 180 days old.

The report also separately identifies:

- **No Password Change Date Recorded**. Unlocked accounts with no recorded `LASTPSWDCHANGE` value (may be migrated or misconfigured)

SSO users (accounts with no PeopleSoft password set) are automatically excluded from this report.

## Table Queried

### PSOPRDEFN — Operator Definitions (User Accounts)

The primary record for PeopleSoft user accounts.

|     Field      |         Description          |                            Values                            |
| -------------- | ---------------------------- | ------------------------------------------------------------ |
| OPRID          | User ID (primary key)        |                                                              |
| OPRDEFNDESC    | User description/name        |                                                              |
| LASTPSWDCHANGE | Date of last password change | Date format                                                  |
| LASTSIGNONDTTM | Date/time of last sign-on    | Datetime format                                              |
| ACCTLOCK       | Account lock status          | `0` = Active, `1` = Locked                                   |
| PTOPERPSWDV2   | Password hash                | Non-empty means password is set (SSO users have no password) |
| OPRCLASS       | Primary permission list      |                                                              |

## Data Flow

```text
1. Fetch ALL users from PSOPRDEFN
   via SearchUsers (batches of 300)
        |
        v
2. Filter:
   - Skip locked accounts (ACCTLOCK = 1)
   - Skip SSO users (no password set)
        |
        v
3. Parse LASTPSWDCHANGE date and compute days since change
        |
        v
4. Categorize into severity buckets:
   CRITICAL: > 365 days since password change
   WARNING:  > 180 days
   INFO:     > staleDays threshold (default 90)
   Plus: No change date recorded
        |
        v
5. Sort each category by days since change (oldest first)
        |
        v
6. Generate Markdown report grouped by severity
```

## Parameters

|  Parameter  | Default |                        Description                        |
| ----------- | ------- | --------------------------------------------------------- |
| `staleDays` | `90`    | Number of days after which a password is considered stale |

## Report Output

The generated report contains:

- **Header** with database name, generation timestamp, and threshold parameter
- **Summary** with total user counts, unlocked count, and counts per severity category
- **CRITICAL section** (if any): Table with user ID (linked), description, last password change date, days since change, last sign-on, permission list
- **WARNING section** (if any): Same table format
- **INFO section** (if any): Same table format
- **No Password Change Date section** (if any): Table with user ID, description, last sign-on, permission list
- **Recommendations** based on which categories have findings

## Interpreting Results

- **CRITICAL findings require immediate action.** Passwords unchanged for over a year are a significant security risk, especially if the accounts are actively used (check the Last Sign-on column).
- **WARNING findings should be scheduled for remediation.** These accounts are approaching a year without a password change.
- **INFO findings indicate policy non-compliance.** The accounts exceed your configured threshold but are not yet at the warning level.
- **No Password Change Date accounts** are often migrated accounts. Verify they are legitimate and consider requiring a password reset.
- **SSO users** (no PeopleSoft password set) are automatically excluded from this report.

## Recommendations

1. Implement PeopleSoft password controls (PTPWDPOLICY) to enforce automatic password expiration. Configure under PeopleTools > Security > Password Configuration > Password Controls.
2. Investigate accounts with no password change date — these may need manual password resets.
