# Search Framework Security Audit

> Audits PeopleSoft Search Framework security: deployed search definitions with no security, sensitive fields in the search index, search administrators, and the search callback user.

---

Site [llms.txt](/llms.txt)

---

<div class="alert alert-primary border shadow-sm mb-4 psl-prospect-cta" role="note">
  <div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3">
    <div>
      <strong>New to psLens?</strong> This page documents one specific report. To see how it runs in the product, what
      the output looks like, and how teams use it in practice, start with a live walkthrough.
    </div>
    <div class="d-flex flex-wrap gap-2">
      <a class="btn btn-sm btn-primary" href="/contact/">Book a Demo</a>
      <a class="btn btn-sm btn-outline-primary" href="/docs/reports/">Browse Reports</a>
    </div>
  </div>
</div><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>
      <li id="row-sql" class="list-group-item py-2 d-none">
        <div class="d-flex justify-content-between align-items-center mb-1">
          <strong id="label-sql">Contextual SQL:</strong>
          <button id="btn-copy-sql" type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 small">
            <i class="bi bi-clipboard me-1"></i>Copy SQL
          </button>
        </div>
        <pre class="mb-0 p-2 bg-light border rounded small"><code id="ctx-sql" style="white-space: pre-wrap; word-break: break-word;">&mdash;</code></pre>
      </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 (data.sql) {
          document.getElementById('ctx-sql').textContent = data.sql;
          document.getElementById('row-sql').classList.remove('d-none');
          const copyBtn = document.getElementById('btn-copy-sql');
          if (copyBtn && navigator.clipboard) {
            copyBtn.addEventListener('click', function () {
              navigator.clipboard.writeText(data.sql).then(function () {
                copyBtn.innerHTML = '<i class="bi bi-check2 me-1"></i>Copied';
                setTimeout(function () {
                  copyBtn.innerHTML = '<i class="bi bi-clipboard me-1"></i>Copy SQL';
                }, 2000);
              });
            });
          }
          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>

## Search Framework Security Audit Report

**Report ID:** `ptsf-security-audit`
**Category:** Security

## Purpose

This report audits the security of the PeopleSoft Search Framework from four tables: the search definitions (`PSPTSF_SD`), their deployments (`PTSF_DEPLOY_OBJ`), their indexed fields (`PSPTSF_SD_ATTR`), and the search instances (`PTSF_SRCH_ENGN`).

A search index is a copy of application data that sits outside the PeopleSoft database. Row-level security does not follow the data there unless the search definition is set up to apply it, and the search engine holds a PeopleSoft sign-on of its own.

## Sections

|  Section  |                      Finding                       | Severity |                       Reads                       |
| --------- | -------------------------------------------------- | -------- | ------------------------------------------------- |
| UNSECURED | Deployed search definition has no security         | info     | `PSPTSF_SD`, `PTSF_DEPLOY_OBJ`, `PSPTSF_SD_PNLGP` |
| SENSITIVE | Sensitive field is indexed                         | warning  | `PSPTSF_SD_ATTR`, `PSQRYFIELD`, `PSPTSF_SRCHATTR` |
| ADMIN     | User holds a search administration permission list | info     | `PSROLECLASS`, `PSROLEUSER`, `PSOPRDEFN`          |
| CALLBACK  | Search callback user holds an administrator role   | warning  | `PTSF_SRCH_ENGN`, `PSROLEUSER`                    |

### UNSECURED

Lists every search definition that is deployed to a search instance and has neither document-level security (`PTSF_ISDOCACL`) nor source-level security (`PTSF_ISSRCACL`). The Search Framework does not filter the results of such a definition by user.

Delivered definitions over public data are deployed this way on purpose: the PeopleTools 8.62 Campus Solutions demo database reports six, including the class search and course catalog definitions. The finding is raised to **warning** when the definition also indexes a sensitive field. When the definition is mapped to a component for keyword search, the component is shown.

### SENSITIVE

Lists every indexed field whose field name is on the sensitive-field list. For a query-based definition the query field is resolved to its record field through `PSQRYFIELD`. For a record-based definition the field comes from `PSPTSF_SRCHATTR`. The finding says whether the field is faceted or displayed in results, and is **critical** when the definition is deployed with no security.

The sensitive-field list is the `mcp.maskedFields` setting, the same list that masks values in MCP query results. The default is `NATIONAL_ID`, `BIRTHDATE`, `BANK_ACCOUNT_NUM`, `ACCOUNT_EC_ID`, `SSN`, `TAX_ID`, `CREDIT_CARD_NBR`, `CVV`, and `DRIVERS_LIC_NBR`. Fields are matched by exact name, and the report prints the list it used.

### ADMIN

Lists the active users who hold `PTPT3100` (Search Administrator), `PTPT3300` (Search Server), or `PTPT5700` (Insights Administrator), with the role that grants it or a note that it is the user's primary permission list. Locked accounts are counted but not listed.

### CALLBACK

The search engine signs in to PeopleSoft as the callback user of each search instance. The section lists every role each callback user holds, and reports a finding when one of them is `PeopleSoft Administrator` or `Security Administrator`.

This section reads `PTSF_SRCH_ENGN`, which also stores the search engine, proxy, and callback passwords. psLens excludes those three columns from the request and never displays them. The table is optional: without it the section is skipped.

## Skipped Sections

A section runs only when every table it reads was read in full. When a table is not whitelisted or its query fails, the section is marked **Skipped** in the summary with the table name and the reason. It is never reported as clean.

## Report Output

- **Header** with the database, the generation timestamp, and the number of search definitions, deployed definitions, and search instances.
- **Warning block** naming any table whose query failed.
- **Summary table** with one row per section: `Clean`, the number found, or `Skipped` with the reason.
- **One section per finding type**, with links to the search definition or user detail page.

## Parameters

This report has no configurable parameters. The sensitive-field list comes from `mcp.maskedFields` in the configuration.

## Change Tracking

The report is diffable. Findings are keyed by section and object, for example `UNSECURED|SSR_CLASS_SEARCH_LFF` or `ADMIN|VP1|PTPT3100`. A scheduled run with notify mode **On change** reports a newly deployed unsecured definition, a newly indexed sensitive field, or a user added to a search administration role.

## Related Reports

- [Search Framework Integrity Audit](/docs/reports/objects/ptsf-integrity-audit/) finds search definitions that reference a missing query, Application Engine program, or application class.
- [PeopleTools Access Audit](/docs/reports/security/peopletools-access/) lists users with Application Designer, Data Mover, and other PeopleTools access.
