Troubleshooting#
How to work through a PROCTOR run that trips, warns or fails, starting from the exit code and ending at the fix. A proctor that trips over the desk has not marked your paper: exit 2 is the proctor's problem, exit 1 is yours, and warnings are notes in the margin.
Read the run in order#
- The exit code.
0: pass (warnings allowed).1: one or more failures.2: tool error. A warning never changes the exit code, so a red run always has a failure behind it. - The policy line. Every report names the policy in use: origin (
option,env,vendororbundled), path,refreshed_onand the age in days. If the run looks wrong, check which policy answered before blaming the checks. - The findings. One line per finding in the text format, each carrying a stable rule id. The Rule ids page maps every id to its check.
Exit 2: the proctor tripped#
A tool error means PROCTOR could not run the checks. The causes it reports:
Bad options#
The run named an option the command does not accept, or a value it cannot read.
- Fix: correct the option against the Commands page.
- Check:
vendor/bin/proctor check --helplists what the command accepts.
The message starts with Refused:#
In CI (any of CI, GITLAB_CI or GITHUB_ACTIONS set to a value other than 0 or false), --policy and --today are refused (exit 2) unless the job sets PROCTOR_ALLOW_OVERRIDES=1: in CI the policy and the date must come from the stack, not from the app being checked, so a pipeline that could pass its own policy or date could grade itself. The message names the option or options that were given.
- Fix: drop
--policyand let the policy resolve from thePROCTOR_POLICYvariable (which stays allowed in CI), then the app'svendor/waynestate/creed/policy/policy.json, then the bundled copy; drop--today, or move the option to a trusted job that setsPROCTOR_ALLOW_OVERRIDES: "1"as a protected or group-level CI variable, never in the app's own.gitlab-ci.yml. - Check: rerun in CI; the run reaches the checks when no refused option is present, or the trusted job allows them. Outside CI the same command runs unchanged.
The message names a policy file#
An explicit source (--policy or PROCTOR_POLICY) that points at a missing or invalid file is an error (exit 2). A vendor copy that cannot be read or parsed, or that is older than the bundled one, is not an error: it is skipped with a note on stderr and the run continues with the next source.
- Fix: correct the path or the file, or drop the explicit source and let the policy resolve from the
PROCTOR_POLICYvariable, then the app'svendor/waynestate/creed/policy/policy.json, then the bundled copy. - Check: rerun and read the policy line; it names the origin, path,
refreshed_onand age of the policy that answered.
The message names policy-overrides.json#
The file is rejected before any check runs when it is malformed or unreadable, when an entry names a check that is not registered in the run, or when an entry 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.
- Fix: repair the JSON, and use a check id that PROCTOR actually runs; the list of valid ids is built from the checks PROCTOR runs, so a new check needs no change to the guard.
- Check: rerun; the file must parse and name only registered checks before any check runs.
composer.json is present but malformed#
A present but malformed or unreadable composer.json is a tool error. A missing composer.json is not: the app gets a verdict, with php-floor failing.
- Fix: repair the JSON, or point
--pathat the application root that has one. - Check: rerun; with a readable composer.json the run reaches the checks.
Failures that mean what they say#
composer-audit fails with not audited:#
Rule id proctor/composer-audit/could-not-run. The audit ran but its stdout was not parseable JSON; the message starts with not audited: and carries the first stderr line, because a green run that audited nothing is worse than a red one.
- Fix: read the stderr line in the message and fix what composer reported. Composer audit runs with
--no-plugins --no-interactionand a minimal explicit environment (PATH plus the variables composer needs to start), so inheritedCOMPOSER_*switches no longer reach it. - Check: rerun the check; only a clean audit with a clean result passes.
composer-audit fails with not audited: because composer is missing#
Rule id proctor/composer-audit/skipped. The message reads not audited: composer executable not found on PATH; install Composer or record an override: there is no composer executable on PATH, so the audit could not run at all. A skipped audit must never look like a pass.
- Fix: install Composer on the runner, or record an active
composer-auditoverride in policy-overrides.json with a reason and an expiry to downgrade the fail to a warning. - Check: rerun; once composer is found, the audit runs and is graded as usual.
composer-audit fails with not audited: because the lock is missing#
Rule id proctor/composer-audit/skipped. The message reads not audited: no composer.lock in the app path; commit the lock or record an override: the app path has no composer.lock, so composer audit --locked has nothing to audit.
- Fix: commit the lock file (
composer update --lockregenerates it), or record an activecomposer-auditoverride in policy-overrides.json with a reason and an expiry to downgrade the fail to a warning. - Check: rerun; with the lock present, the audit runs and is graded as usual.
composer-audit fails with no advisory listed#
Rule id proctor/composer-audit/exit-code. 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, and only a clean exit with a clean result passes.
- Fix: address what Composer blocked: the block is real, not a PROCTOR misread.
- Check: run with
--format=jsonand read the finding.
composer-audit fails because an ignore entry has no matching override#
Rule id proctor/composer-audit/unbacked-ignore. The message reads ignore entry "<entry>" in composer.json has no matching override; record it in policy-overrides.json with a reason and an expiry: the app silences an audit result in its own composer.json, and no active composer-audit override in policy-overrides.json backs that entry. An ignore without a dated approval never expires, so each unbacked entry fails instead of warning.
- Fix: record an active
composer-auditoverride in policy-overrides.json that backs the entry: itsadvisoryfield equals the entry's advisory id case-insensitively, itspackagefield matches the entry's package, or the override carries no scope at all (the only shape that backs an ignore-severity level). Remove the ignore entry from composer.json when the override expires. - Check: rerun; the entry earns no
unbacked-ignorefail, the count warning (proctor/composer-audit/ignore-entries) still reports it, and the backing override is not flaggedproctor/overrides/unused.
lock-committed fails#
composer.lock is missing from the app path.
- Fix: commit the lock file.
- Check: rerun; with the lock present, a
content-hashthat no longer matches composer.json warns (proctor/lock-committed/out-of-sync) instead of failing, so regenerate the lock when the warning appears and the warning never changes the exit code.
destructive-deploy fails#
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. Scanned: .sh and .php files under a top-level deployments/, deploy_scripts/, deploy/ or scripts/ directory, the top-level build.sh, release.sh and .gitlab-ci.yml, and the scripts section of composer.json; shell comments are ignored.
- Fix: run plain
php artisan migrate --forcein the deploy. - Check: rerun; the check passes when no scanned script drops or rebuilds the database.
php-floor fails and there is no composer.json#
Intended behavior: a missing composer.json is not a tool error, so the app gets a verdict, with php-floor failing.
- Fix: point
--pathat the application root that carries the composer.json. - Check: rerun against the right path.
Warnings that deserve a look#
proctor/policy-age#
The policy in use is more than 60 days old (its refreshed_on date is more than 60 days before --today), or a non-bundled policy has no refreshed_on date. It exists so a stopped CREED refresh robot is visible instead of silent.
- Fix: refresh the policy:
composer update waynestate/creedfor the vendor copy, or point--policyorPROCTOR_POLICYat a current file. Warning only: an old policy never fails a run, and the bundled policy may omit the date.
proctor/overrides/expiring and proctor/overrides/unused#
An active entry in policy-overrides.json that expires within 14 days of --today (the expiry day counts) warns expiring, so an approval that is about to lapse is visible instead of silent. An active entry that matches no failure warns unused, so a stale approval is removed instead of rotting.
- Fix: renew the entry with a new
expiresdate and approver, or delete it. A warning never changes the exit code; an expired entry, however, fails theoverridescheck.
proctor/repo-url-credentials#
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, a bare token user of 20+ characters, a private_token, access_token, token or password query parameter, or a .npmrc line that sets _authToken, _auth or _password to a literal value (${SOME_VAR} is fine). The secret is masked as *** in the message, so the report itself is safe to hand back.
- Fix: move the token to
auth.json, which is gitignored, or a CI variable, and use one registry URL without credentials. - Check: rerun; the warning clears when no scanned URL carries a literal credential.
proctor/ci-pipelines/composer-audit-off#
The pipeline environment switches Composer's audit or blocking policy off: COMPOSER_NO_AUDIT or COMPOSER_NO_BLOCKING set to anything but an empty string, 0 or false, or COMPOSER_AUDIT_ABANDONED set to ignore, in the GitLab top-level or job variables: (a scalar or a map with a value key) or the GitHub top-level, job or step env:.
- Fix: remove the switch from the pipeline environment. A warning, never a fail, for stack packages and other projects alike.
A dev-dependency advisory is only a warning#
An advisory whose package composer.lock lists only in packages-dev is a warning at any severity, with the message ending in [dev dependency]. It still needs fixing: dev tools run in CI, and a vulnerable test runner is a real attack surface even though it is not in the deployed image.
- Fix: update the dev dependency the way you would any other (
composer update vendor/package --with-dependencies), or record a datedcomposer-auditoverride in policy-overrides.json if the upgrade is blocked. - Check: open
composer.lockand look for the package name underpackages-dev; if it also appears underpackages, it counts as runtime and the advisory fails at or above the threshold as usual.
portability warnings from bootstrap/cache/#
In a Laravel app (an artisan file at the project root) the top-level bootstrap/cache/ is skipped, so the files php artisan optimize writes there (config.php, routes-v7.php, services.php, packages.php, events.php) do not raise warnings and hide real findings. If you still see them, the app has no artisan at the root: a raw-PHP app that keeps its own bootstrap/cache/ folder is scanned as before.
- Fix: put
artisanat the project root, or move the generated output out of the scanned directories. - Check: rerun; the generated cache files stop appearing once the app is recognized as Laravel.
Results that follow the environment#
unpatched disagrees with the deploy#
--php-runtime is the PHP version that will serve the app (X.Y.Z), used by the unpatched check. CI should pass the deploy image's PHP, not the runner's.
- Fix: pass
--php-runtimematching the deploy image in CI. - Check: rerun with the flag; the runtime finding follows the version you pass.
The same app gives different results on different days#
--today overrides today's date (YYYY-MM-DD) so runs are deterministic; policy-age and the overrides expiring window both compare against it.
- Fix: pin
--todayin CI so a policy refresh or an override renewal does not change the verdict mid-week. - Check: rerun twice with the same
--today; the findings match.
An offline run reports proctor/wayneui-outdated/skipped#
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.
- No fix needed: the version comparison resumes when the release list can be read, and an offline run never fails in either mode, because an unknown latest is always just the info skipped finding.
You are on an older PROCTOR#
Some symptoms were bugs, fixed in a release. Check your version first:
composer show waynestate/proctor
- Exit 255 with
Allowed memory size exhaustedwhile scanning a large app: fixed in 0.2.4. Each file's syntax tree is dropped before the next file is parsed, andbin/proctorraises a memory limit below 512M to 512M (a higher limit or -1 is left alone). - Every advisory named
unknown: fixed in 0.2.8. PROCTOR reads Composer's real audit JSON, and each finding reads<package> <version>: <CVE or advisory id> <title> (<severity>). - The app's ignore list matched nothing: fixed in 0.2.8.
config.policy.advisories.ignore-id,config.policy.advisories.ignore(by package) and the legacyconfig.audit.ignoreare read in both their list and map shapes, matching any of the advisory's ids. - A
?in the text report is not corruption: since 0.3.1, terminal control characters in app-supplied strings (finding messages, override reasons and approvers) print as a visible?. Clean the string at the source.