waynestate / proctor
A framework-free PHP CLI that measures a PHP app against the version policy.
Requires
- php: ^8.3
- composer/semver: ^3.4
- nikic/php-parser: ^5.6
- symfony/console: ^7.1
- symfony/yaml: ^7.1
Requires (Dev)
- humbug/box: 4.7.1
- laravel/pint: ^1.32
- league/commonmark: ^2.10
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- waynestate/pinstripe: ^0.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PROCTOR
Policy Rules Of Code, Tested On Release. The invigilator your composer.lock didn't know it needed: no phones, no EOL PHP.
PROCTOR is a framework-free PHP CLI that measures a PHP application against
the WSU version policy. It reads the app's composer.json and composer.lock,
decides the support state of the app's PHP floor from a policy file, and
reports each check as pass, warning, info or fail. The checks and their rules
come from the stack's version policy decision and from the policy file
passed with --policy; PROCTOR itself holds no policy of its own.
Note
Not to be confused with: code style is Pinstripe, CI is Preflight. PROCTOR grades an app against CREED's rules.
Checks: composer ci (the same command runs on GitLab and GitHub). Front page: site/index.html.
On Citadel it is served by the raw-PHP deploy layer in deploy/: a Squire kit whose front controller serves site/ behind HELMET's security headers. For local development, keep serving site/ directly.
Documentation: proctor.apps.wayne.edu/docs.
Where the code lives
The canonical repository is on nougat: enterprise-apps/proctor. git.wayne.edu and GitHub (waynestate/proctor) are read-only mirrors of it. Open issues and merge requests on nougat, not on the mirrors. This moves to git.wayne.edu once it has CI runners. Your proctor keeps one answer sheet, and this is it; the mirrors hold photocopies.
Install
composer require waynestate/proctor --dev
The command is then vendor/bin/proctor.
Single-file download
The single-file PHAR is how Preflight and apps without Composer access run
PROCTOR as one file, and from the next release it is attached to each GitLab
Release with its proctor.phar.sha256 checksum file. Both files come from the
release-phar job in .gitlab-ci.yml; the lead tags, and no tag means no
release. The composer require install above stays the current method; the
phar is the one-file alternative.
Download both files for the tag you want, verify the checksum, then run it.
Replace <release> with the base URL of the release's asset links (the
release page lists both files).
Linux / macOS (sh), one line:
curl -fsSL -o proctor.phar "<release>/proctor.phar" -o proctor.phar.sha256 "<release>/proctor.phar.sha256" && sha256sum -c proctor.phar.sha256 && php proctor.phar --version
Windows (PowerShell), one line, no bash needed:
Invoke-WebRequest "<release>/proctor.phar" -OutFile proctor.phar; Invoke-WebRequest "<release>/proctor.phar.sha256" -OutFile proctor.phar.sha256; if ((Get-FileHash proctor.phar -Algorithm SHA256).Hash -ne (Get-Content proctor.phar.sha256 -TotalCount 1).Trim().Split(' ')[0]) { throw "sha256 mismatch" }; php proctor.phar --version
Until the first tagged release that runs the job, build it from a checkout
with the script for your OS (both need Docker, and no PHP or Composer on the
host): .\scripts\build-phar.ps1 on Windows, scripts/build-phar.sh on
Linux / macOS. Each script writes build/proctor.phar and verifies it by
printing PROCTOR's version.
Usage
vendor/bin/proctor check [--path=.] [--format=text|json|codequality|sarif] [--policy=<file>] [--today=YYYY-MM-DD] [--php-runtime=X.Y.Z]
--path: path to the application to check (default.).--format: output format:text(default),json,codequality, orsarif.--policy: path to the CREED-format policy file. Optional: without it, PROCTOR resolves the policy from thePROCTOR_POLICYenvironment variable, then the app'svendor/waynestate/creed/policy/policy.json, then the copy bundled with PROCTOR (see Policy).--today: override today's date (YYYY-MM-DD) so runs are deterministic.--php-runtime: the PHP version that will serve the app (X.Y.Z), used by theunpatchedcheck. CI should pass the deploy image's PHP, not the runner's.
Checks
php-floor: fails when the lowest PHP version allowed byrequire.phpis past security support plus the grace window, when composer.json has no usable php constraint, or when there is no composer.json at all.unpatched: warns when a locked component is behind the policy'slatest_patchfor its version line, and passes when it is current. Reads laravel from composer.lock (laravel/framework), vue and inertia from package-lock.json (vue,@inertiajs/vue3), and the PHP runtime from--php-runtime. No finding when the component, its version line, orlatest_patchis absent from the policy.unbounded-constraints: fails for every runtime constraint that is*, names a dev- branch, carries@dev, has a lower bound with no upper bound, or matches the policy'sbanned_constraints; warns onpathandvcsrepositories, on aminimum-stabilitybelow stable, and on a repository URL that is plain http.require-dev,php,ext-*andlib-*are not checked. No open-ended answers in this hall.composer-config: warns when composer.json opens a door Composer closes by default:allow-pluginsset totrue,secure-httpset tofalse,lockset tofalse,audit.block-insecureset tofalse,config.policyset tofalse,policy.malwareset tofalseorpolicy.advisoriesset tofalse; notes whenrequire.phpis set withoutconfig.platform.php. Warning and info only, never a fail, so an app that passes today keeps passing.lock-committed: fails when composer.lock is missing from the app path. When the lock is present and itscontent-hashis a real 32-hex hash, it warns (proctor/lock-committed/out-of-sync) when the hash no longer matches composer.json, so a stale lock that still installs is visible instead of silent; the warning never changes the exit code.portability: warns when code hard-codes a platform fact: an absolute host path, a hard-coded proxy address, a local-disk session store, or a php_value/php_flag line in .htaccess. PHP files are matched on the syntax tree (nikic/php-parser with names resolved), not with regular expressions, so a comment or string that merely mentions a banned call is not flagged and a call split over several lines, or hidden behind ause function ... as ...alias, is. A call through a variable ($f()) is reported as an info note, never as a pass. Scanned: every *.php file, every file under the top-levelconfig/directory, and every.htaccess. Skipped at any depth:vendor/,node_modules/,storage/,tests/and.git/; in a Laravel app (anartisanfile at the project root) also the top-levelbootstrap/cache/, which holds the generated output ofphp artisan optimize(config.php, routes-v7.php, services.php, packages.php, events.php) instead of app code. A raw-PHP app that keeps its ownbootstrap/cache/folder is scanned as before.mysql56: warns when migrations or app code use MySQL features the shared 5.6 server rejects: JSON columns (indatabase/) and JSON queries, generated columns,renameColumn/renameIndex, vector, invisible and spatial columns,Schema::hasColumn-style introspection, raw SQL with 5.7+ syntax, the upsert row alias, indexed strings over 191 characters, date columns defaulting to the current time, and thedb:show/db:monitorcommands. Citadel's shared MySQL server is 5.6, so migrations must avoid newer features. Warnings, not failures: an app on its own newer database is not affected. Fix by avoiding the feature (a rawALTER TABLE ... CHANGEinstead ofrenameColumn,timestampinstead of a date column defaulting to now,Schema::defaultStringLength(191)inboot()) or by moving the app to its own database. HTTP->json()calls are not flagged, and a long string only warns when the same migration indexes it.destructive-deploy: fails when a deploy script runs an artisan command that drops or rebuilds the database (migrate:fresh,migrate:refresh,migrate:reset,db:wipe), which erases production data on every deploy. Scans.shand.phpfiles under a top-leveldeployments/,deploy_scripts/,deploy/orscripts/directory, the top-levelbuild.sh,release.shand.gitlab-ci.yml, and thescriptssection of composer.json; shell comments are ignored. Fix by running plainphp artisan migrate --forcein the deploy.ci-pipelines: for stack packages (composer.jsonnamestarting withwaynestate/), fails when.gitlab-ci.ymlor.github/workflows/ci.ymlis missing or runs nocomposer ci; for any other project, a missing or incomplete.gitlab-ci.ymlis a warning and.github/workflows/ci.ymlis optional. A private stack package that setsextra.proctor.github-mirrortofalsein composer.json must never be on GitHub: a missing.github/workflows/ci.ymlthen gives no finding, and a present one warns (proctor/ci-pipelines/github-private) along with the warning-only rules. Warns when a GitHubuses:ref is not a 40-hex commit SHA. Warns (proctor/ci-pipelines/composer-audit-off) when the pipeline environment switches Composer's audit or blocking policy off:COMPOSER_NO_AUDITorCOMPOSER_NO_BLOCKINGset to anything but an empty string,0orfalse, orCOMPOSER_AUDIT_ABANDONEDset toignore, in the GitLab top-level or jobvariables:(a scalar or a map with avaluekey) or the GitHub top-level, job or stepenv:. A warning, never a fail, for stack packages and other projects alike. The proctor fails the stack's own exam halls, and only leaves a note for everyone else's.preflight-pinned: warns, never fails, when an app includes a Preflight component at a moving ref or re-declares what an included Preflight job runs. A Preflight include is aninclude:entry whosecomponent:path contains/preflight/; when the ref after the last@is not a 40-lowercase-hex commit SHA it warns (proctor/preflight-pinned/unpinned), and a tag, a branch,~latestor no@at all all count as moving. Pin the commit SHA of the release tag; the tag itself can ride in a trailing comment. When the app declares one of the Preflight job names (proctor,php-test,composer-audit,sbom,pint,phpstan,eslint,renovate,package-publish) withscript,before_scriptorafter_scriptwhile a Preflight component is included, it warns (proctor/preflight-pinned/script-override). With nothing to flag it passes:Preflight includes are pinned, orno Preflight components includedwhen there is no pipeline, a parse error or no Preflight include. The proctor checks the boarding pass against the manifest, so the app cannot board a different flight mid-run.composer-audit: fails for advisories at or above the policy'saudit_severity_threshold, warns for lower ones, and fails with a message that starts withnot audited:when the composer executable is not on PATH or the app has no composer.lock (rule idproctor/composer-audit/skipped): install Composer, commit the lock, or record an activecomposer-auditoverride in policy-overrides.json with a reason and an expiry to downgrade the fail to a warning. An advisory on a package that composer.lock lists only in packages-dev is a warning at any severity (its message ends in[dev dependency]), while a runtime advisory fails at or above the threshold. When the audit runs but its stdout is not parseable JSON, the check fails (rule idproctor/composer-audit/could-not-run) with a message that starts withnot audited:and carries the first stderr line, because a green run that audited nothing is worse than a red one. Each finding names the package, the version locked in composer.lock, the advisory's CVE (or its advisory id when there is no CVE), its title and its severity. An advisory the app ignores in its own composer.json (config.policy.advisories.ignore-id,config.policy.advisories.ignore, or the legacyconfig.audit.ignore) is a warning, not a fail, and so is anything composer has already moved to its ignored section; abandoned packages are warnings too. A package pattern inconfig.policy.advisories.ignoremay use*, matched the way composer does, case-insensitively. Since Composer 2.10 the audit exits 1 when anything failed it, so a non-zero exit whose JSON lists nothing PROCTOR can read (a malware or policy block, or an error) is itself a fail (proctor/composer-audit/exit-code), and only a clean exit with a clean result passes. A warning (proctor/composer-audit/ignore-entries) reports the number of ignore entries in composer.json:config.policy.advisories.ignore-id,config.policy.advisories.ignore,config.policy.advisories.ignore-severity,config.policy.abandoned.ignore, or the legacyconfig.audit.ignore,config.audit.ignore-severityandconfig.audit.ignore-abandoned(the two legacy fallbacks count only while their matchingconfig.policyblock is absent), so they move to the dated override file. An ignore entry that no activecomposer-auditoverride in policy-overrides.json backs is a fail of its own (proctor/composer-audit/unbacked-ignore): an override backs the entry when itsadvisoryfield equals the entry's advisory id case-insensitively, itspackagefield matches the entry's package the way composer matches patterns, or it carries no scope at all, and a backing override is not reported unused. The proctor checks the invigilator's mark against its own answer sheet, and refuses to sign a green for a red pen.composer-version: fails when the Composer on PATH carries the command-injection holes CVE-2024-35241 and CVE-2024-35242 (2.2.x below 2.2.24, or any other 2.x below 2.7.7) or is Composer 1, which is end of life; warns when composer cannot be run. The invigilator checks its own pen: GHSA-47f6-5gq3-vx9c and GHSA-v9qv-c7wm-wgmf.repo-url-credentials: warns when a credential is written into a package source URL: a deploy token in the userinfo of a composer.json repository URL or a composer.lock source/dist URL (https://gitlab+deploy-token-12:***@host/group/lib.git), a bare token user of 20+ characters, aprivate_token,access_token,tokenorpasswordquery parameter, or a.npmrcline that sets_authToken,_author_passwordto a literal value (${SOME_VAR}is fine). The secret is masked as***in the message, so the report itself is safe to hand back. Warning, not failure. Fix by moving the token toauth.json, which is gitignored, or a CI variable, and using one registry URL without credentials. The proctor spots the answer key taped inside the desk before the student does.instant-client-committed: warns when Oracle Instant Client is committed to the repo: aninstantclient*.ziporinstantclient*.rpmarchive, any file under a directory namedinstantclientorinstantclient_*(an unzipped install), or a loose client library such aslibclntsh.so,libociei.sooroci.dll. Only committed files count: the git-tracked files when the app path sits in a git repository and git can be run, otherwise the files on disk. Skipped at any depth:vendor/,node_modules/and.git/. One warning per match: the archive, the topmost matching directory, or the loose file, each reported with its total size in MB. Warning, not failure: Citadel's images already include Instant Client, and the WSU Sail and Docker images will too. Fix by deleting it, runninggit rm -r --cachedon the path PROCTOR reports, and adding that path to .gitignore. The proctor takes the 60-to-126-MB textbook off the student's desk: the exam hall already keeps a copy on the shelf.wayneui-forked-views: warns when forked copies of WayneUI are committed: any file underresources/views/vendor/wayne-ui/(the published 1.x views) orpublic/vendor/wayne-ui/(the published 1.x assets). Those copies silently override the package, so an app keeps old layouts after a WayneUI upgrade and nobody sees it; WayneUI's MIGRATING.md says to delete them. Only committed files count: the git-tracked files when the app path sits in a git repository and git can be run, otherwise the files on disk. Skipped at any depth:node_modules/and.git/; skipped at the top level only:vendor/(Composer's), so thevendorsegment insideresources/views/vendor/wayne-ui/still matches. One warning per folder, with its file count. Warning, not failure: a new check must not turn a passing app into a failing one. Fix by deleting the folder and runninggit rm -r --cachedon it; if you changed the forked views on purpose, compare them with the package views first. The proctor confiscates the photocopied syllabus: the current one, with the errata, is already pinned to the board.wayneui-outdated: warns when the WayneUI an app carries is behind the latest release. It reads the installed version from composer.lock (waynestate/wayne-ui), package-lock.json (@waynestate/wayne-ui-css,@waynestate/wayne-ui-vue) and committed copies ofwayne-ui.css(the version in the banner of the first 400 bytes), asks composer once per run for the release list (composer show waynestate/wayne-ui --all --format=json), and warns, per source, when the installed version is behind the highest stable release of the same major. Pre-releases (-dev, alpha, beta, RC) and other majors are ignored. A locked WayneUI 1.x (cit-web-infra/wayne-ui) gets its own warning with no lookup: 1.x is Bootstrap 3 and predates the comparison. Only committed files count as copies: the git-tracked files when the app path sits in a git repository and git can be run, otherwise the files on disk; skipped at any depth:node_modules/and.git/, and the top-levelvendor/. When the lookup cannot run (no composer on PATH, offline, or no parsable versions) the check reports one info finding with the skipped marker and nothing else, so an offline run never breaks; an app with nothing WayneUI-related runs no process at all. By default the check warns, not fails: being out of date must not turn a passing app into a failing one. An app that wants a hard stop on its pasted stylesheets setswayneui.copiestofailin its composer.json, underextra.proctornext togithub-mirror; the default iswarn. Infailmode a behind committed copy is a failure, with a sentence naming the setting appended to its message, while findings from composer.lock and package-lock.json stay warnings: only committed copies are affected by the setting, and an offline run never fails in either mode, because an unknown latest is always just the info skipped finding. Fix by updating the lock file (composer update waynestate/wayne-ui, ornpm updatefor the npm packages) or by replacing the pasted copy from the release. The proctor marks the paper against the current syllabus, not the one the student photocopied last semester.overrides: fails for every expired entry in policy-overrides.json, and warns (proctor/overrides/expiring) for an active entry that expires within 14 days of--today(the expiry day counts), so an approval that is about to lapse is visible instead of silent. An active entry that matches no failure warns (proctor/overrides/unused) so a stale approval is removed instead of rotting; the warning never changes the exit code. An entry that names a check that is not registered in the run, or that names theoverridescheck itself, is a tool error (exit 2): the file is rejected before any check runs.policy-age: warns when the policy in use is more than 60 days old (itsrefreshed_ondate is more than 60 days before--today), or when a non-bundled policy has norefreshed_ondate. Warning only: an old policy never fails a run, and the bundled policy may omit the date. It exists so a stopped CREED refresh robot is visible instead of silent.
Exit codes
0: pass (warnings allowed)1: one or more failures2: tool error (bad options, unreadable policy, a present but malformed or unreadable composer.json, or an overrides file that is malformed, unreadable, or names a check that is not registered or the overrides check itself). A missing composer.json is not a tool error: the app gets a verdict, with php-floor failing.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Helvetica, Arial, sans-serif','primaryColor':'#F4F1EA','primaryTextColor':'#1f2a33','primaryBorderColor':'#0C5449','lineColor':'#8A9A94'}}}%%
flowchart LR
run["proctor check"] --> ok{"options and policy readable?"}
ok -- no --> two["exit 2: the proctor tripped"]
ok -- yes --> fails{"any FAIL?"}
fails -- yes --> one["exit 1: resit"]
fails -- no --> zero["exit 0: pass, warnings allowed"]
classDef pass fill:#0C5449,stroke:#0C5449,color:#F4F1EA
classDef fail fill:#9B1C2E,stroke:#9B1C2E,color:#F4F1EA
classDef trip fill:#F4F1EA,stroke:#9B1C2E,color:#9B1C2E,stroke-dasharray:4 3
class zero pass
class one fail
class two trip
Loading
Formats
text: a header line with the tool and the versions of the external tools used (composer), then one line naming the policy in use (origin, path, refresh date), then one line per finding, then one line per override, then a RESULT line.json: one machine-readable object with tool, version, path, today, result, exit, tools, policy (origin, path, refreshed_on, age_days), findings and overrides. An advisory finding carries anadvisoriesarray with one entry per advisory (package, id, cve, title, link, severity); other findings have no such key.codequality: GitLab's Code Quality JSON: a single array of findings, so a merge request shows them.sarif: a SARIF 2.1.0 log: one run, one rule per check, one result per non-pass finding, so GitHub code scanning and other SARIF viewers show them.
Rule ids
Every finding carries a stable, namespaced rule id: proctor/<check-id> for a
check's default finding, proctor/<check-id>/<rule> for a named rule. The id
is the finding's identity in the machine formats: rule_id in json,
ruleId in sarif, and check_name in codequality. A rule keeps the
same id across runs and across formats even when its message changes.
The full set, grouped by check:
| Check | Rule id |
|---|---|
php-floor |
proctor/php-floor |
unpatched |
proctor/unpatched |
lock-committed |
proctor/lock-committed |
lock-committed |
proctor/lock-committed/out-of-sync |
destructive-deploy |
proctor/destructive-deploy |
overrides |
proctor/overrides |
overrides |
proctor/overrides/expiring |
overrides |
proctor/overrides/unused |
unbounded-constraints |
proctor/unbounded-constraints |
unbounded-constraints |
proctor/unbounded-constraints/unbounded |
unbounded-constraints |
proctor/unbounded-constraints/repository |
unbounded-constraints |
proctor/unbounded-constraints/minimum-stability |
unbounded-constraints |
proctor/unbounded-constraints/insecure-url |
composer-config |
proctor/composer-config |
composer-config |
proctor/composer-config/allow-plugins-all |
composer-config |
proctor/composer-config/secure-http-off |
composer-config |
proctor/composer-config/lock-off |
composer-config |
proctor/composer-config/audit-blocking-off |
composer-config |
proctor/composer-config/policy-off |
composer-config |
proctor/composer-config/policy-malware-off |
composer-config |
proctor/composer-config/policy-advisories-off |
composer-config |
proctor/composer-config/platform-php-unset |
portability |
proctor/portability |
portability |
proctor/portability/host-path |
portability |
proctor/portability/proxy-address |
portability |
proctor/portability/local-session |
portability |
proctor/portability/htaccess-php-setting |
portability |
proctor/portability/parse-error |
portability |
proctor/portability/dynamic-call |
mysql56 |
proctor/mysql56 |
mysql56 |
proctor/mysql56/json |
mysql56 |
proctor/mysql56/generated |
mysql56 |
proctor/mysql56/rename |
mysql56 |
proctor/mysql56/newer-column |
mysql56 |
proctor/mysql56/introspection |
mysql56 |
proctor/mysql56/raw-sql |
mysql56 |
proctor/mysql56/string-length |
mysql56 |
proctor/mysql56/current-date |
mysql56 |
proctor/mysql56/command |
mysql56 |
proctor/mysql56/upsert-alias |
mysql56 |
proctor/mysql56/string-default |
mysql56 |
proctor/mysql56/parse-error |
ci-pipelines |
proctor/ci-pipelines |
ci-pipelines |
proctor/ci-pipelines/gitlab-missing |
ci-pipelines |
proctor/ci-pipelines/gitlab-parse |
ci-pipelines |
proctor/ci-pipelines/gitlab-no-ci |
ci-pipelines |
proctor/ci-pipelines/github-missing |
ci-pipelines |
proctor/ci-pipelines/github-parse |
ci-pipelines |
proctor/ci-pipelines/github-no-ci |
ci-pipelines |
proctor/ci-pipelines/github-private |
ci-pipelines |
proctor/ci-pipelines/unpinned-uses |
ci-pipelines |
proctor/ci-pipelines/composer-audit-off |
preflight-pinned |
proctor/preflight-pinned |
preflight-pinned |
proctor/preflight-pinned/unpinned |
preflight-pinned |
proctor/preflight-pinned/script-override |
composer-audit |
proctor/composer-audit |
composer-audit |
proctor/composer-audit/advisory |
composer-audit |
proctor/composer-audit/abandoned |
composer-audit |
proctor/composer-audit/skipped |
composer-audit |
proctor/composer-audit/could-not-run |
composer-audit |
proctor/composer-audit/exit-code |
composer-audit |
proctor/composer-audit/ignore-entries |
composer-audit |
proctor/composer-audit/unbacked-ignore |
composer-version |
proctor/composer-version |
composer-version |
proctor/composer-version/not-found |
composer-version |
proctor/composer-version/could-not-determine |
repo-url-credentials |
proctor/repo-url-credentials |
instant-client-committed |
proctor/instant-client-committed |
wayneui-forked-views |
proctor/wayneui-forked-views |
wayneui-outdated |
proctor/wayneui-outdated |
wayneui-outdated |
proctor/wayneui-outdated/skipped |
policy-age |
proctor/policy-age |
tests/RuleIdContractTest.php keeps this table in step with the checks: a rule
id that is emitted but not listed here fails the suite.
GitHub code scanning
The sarif format feeds GitHub code scanning on the GitHub mirror. Upload the
log with the official action, pinned to a full commit SHA:
name: proctor on: [push, pull_request] permissions: security-events: write jobs: proctor: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.4' - run: composer require waynestate/proctor --dev # policy.json is the CREED policy file - run: vendor/bin/proctor check --format=sarif --policy=policy.json > proctor.sarif - uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: sarif_file: proctor.sarif
PROCTOR now files the mark sheet in the format the head office already reads. No more translating the exam results by hand.
Overrides
An app can carry a policy-overrides.json file next to its composer.json:
[
{
"check": "lock-committed",
"reason": "lock is generated by CI and not committed",
"expires": "2027-01-31",
"approved_by": "jane@example.wayne.edu"
}
]
A composer-audit override may name the one thing it covers with two
optional fields:
[
{
"check": "composer-audit",
"reason": "upgrade is blocked until the next release window",
"expires": "2027-01-31",
"approved_by": "jane@example.wayne.edu",
"advisory": "CVE-2024-52301",
"package": "laravel/framework"
}
]
advisory is an advisory id or CVE, matched case-insensitively against the
finding's advisory id or CVE. package is a package name, with * matching
any run of characters the way composer does, matched case-insensitively
against the audited package. When both are present, both must match the same
advisory. Without either field the override covers every fail of the check,
as before. The fields are only valid on composer-audit: on any other
check, or with a value that is not a non-empty string, PROCTOR rejects the
file (exit 2). A scoped override never covers a fail that carries no
advisory data (such as the exit-code fail), and a scoped override that
matches no failure earns the proctor/overrides/unused warning with its
scope in the text.
An expired override fails the overrides check. An active override turns the
matching fail findings of its check into warnings. An override never waives a failure whose
state is EOL plus grace (eol_grace_expired); that failure stands (the stack's version policy decision).
PROCTOR rejects the file (exit 2) when an entry names a check that is not
registered in the run, or names the overrides check itself: an override
cannot exempt the check that polices overrides, and an entry for a check
that does not exist would silently do nothing. The list of valid check ids
is built from the checks PROCTOR runs, so a new check needs no change to
the guard. An active override that expires within 14 days of --today
(the expiry day counts) gets a warning finding
(proctor/overrides/expiring) instead of lapsing without a trace, and an
active override whose check raised no failure gets a warning finding
(proctor/overrides/unused) so a stale approval is removed instead of
rotting. A warning never changes the exit code.
Policy
--policy takes a CREED-format policy file: schema_version 1, component
versions with released, active_until and security_until dates, and rules
(audit_severity_threshold, banned_constraints, eol_grace_days). The
policy-file wiring to waynestate/creed is described below.
Without --policy, PROCTOR resolves the policy in this order (first match
wins):
- the
PROCTOR_POLICYenvironment variable, naming a policy file; <app>/vendor/waynestate/creed/policy/policy.json, when it exists and is at least as fresh as the bundled copy (same or newerrefreshed_on);- the policy bundled with PROCTOR (
src/Creed/policy.json).
An app that installs CREED gets its policy automatically:
composer require waynestate/creed --dev
A vendor copy that cannot be read or parsed is skipped with a note on stderr
and the run continues with the next source; a vendor copy older than the
bundled one is skipped the same way. An explicit source (--policy or
PROCTOR_POLICY) that points at a missing or invalid file is still an error
(exit 2).
Every report names the policy it used: origin (option, env, vendor or
bundled), path, refreshed_on and the age in days. The policy-age check
warns when the policy is more than 60 days old, or when a non-bundled policy
has no refreshed_on date, so a stopped CREED refresh is visible instead of
silent.
PROCTOR carries a generated copy of the CREED reader under src/Creed/
(namespace Waynestate\Proctor\Creed), so it never depends on the CREED
package at runtime. composer creed-copy:check verifies the copy against the
CREED checkout named by CREED_PATH (default ../creed); refresh it by
running CREED's bin/export.php against src/Creed/.
Proof
Proof runs against real Wayne State applications are on a private site, proof.apps.wayne.edu/proctor, behind a Wayne State employee sign-in.
Docs
The documentation lives at proctor.apps.wayne.edu/docs and is built from docs/ by Squire's docs builder:
composer docs
renders the pages into site/docs/ and fills the generated command and environment reference from the CLI and the source. The pages are committed, so the site stays static. Check without writing:
composer docs:check
which runs as part of composer ci.
Running the tests in Docker
Maintainers: how to build, test and release this project, and where its data comes from, is in MAINTAINING.md.
No PHP is needed on the host:
docker run --rm -v "${PWD}:/app" -w /app composer:2 composer install --no-interaction
docker run --rm -v "${PWD}:/app" -w /app php:8.4-cli vendor/bin/phpunit
The same works with php:8.3-cli in place of php:8.4-cli.
Strongly recommended: a git.wayne.edu instance runner.
License
MIT, Wayne State University, Enterprise Applications.
Seated in the same hallWayneUI and the stack. Each one sits its own exam.
scripts/readme-art. Pencils down.

