Security
A dependency bot holds a token that can write to every repository it scans. And it reads configuration from all of them. These are the lines pinup holds, and what a repository can and cannot make the bot do.
- Reports: security reports go to the address in the repository's
SECURITY.md, not as an issue
The invariants
Every run produces a plan before any write
What changes is known in bytes before the first byte changes.
Apply consumes byte-range edits, never dependencies
Nothing downstream of the plan can invent a change; YAML and JSON are never re-serialised.
The core decides, plugins apply
A task runs in a scope it may not leave; a path outside discards its whole result.
The stricter of two labels wins
An analyzer loosens an automerge only with trustEffective: true.
Plugins get no credentials
A task sees PATH, LANG, TZ and whatever PINUP_PLUGIN_ENV names; never the platform token, never the signing key.
A plan explains why nothing happens
A held update carries its reason; a refused command is named, not skipped.
The platform token
The token is bound to the instance's host and, on GitLab, to the API paths the bot itself calls: releases, tags, raw files, packages, the group Composer registry, its own user. A URL a repository configuration names on the instance (a customDatasources template, a registryUrls entry) is requested without it. Otherwise any developer in any scanned repository could point the bot's api token at /api/v4/groups/<id>/variables and read the result as a “release list”.
Registry access exists only for the estate's own registry (PINUP_REGISTRY_HOST), exchanged at the instance's token realm over TLS, scoped to the one repository a lookup needs. Every other registry is asked anonymously.
The GitHub token is bound to api.github.com (or the Enterprise host). A redirect away from a platform's host is refused, not followed: net/http drops Authorization across hosts, a PRIVATE-TOKEN header it does not.
No token ever lands in a file or a URL. git asks for credentials through GIT_ASKPASS, which is the pinup binary itself, and it answers only a prompt for the platform's host.
What a repository can decide
A repository's own configuration file can disable itself, change versionings and registries, add rules, request tasks. It cannot:
- run a command the runner's
PINUP_ALLOWED_COMMANDSdoes not admit (checked on the compiled command, after templating) - raise
executionTimeout(that is the runner'sPINUP_EXECUTION_TIMEOUT) - reach the platform token through a URL it names
- write outside a task's declared scope
- be automerged on a label the runner's rules do not trust
Commits
Every commit is signed as the runner is configured (openpgp or ssh with PINUP_SIGNING_KEY), by an identity the key vouches for (CheckIdentity rejects a key that does not name the author). The run reads the platform's verdict on its own commits. Unsigned works only with the explicit word none.
A branch somebody else committed to is theirs: the run neither rebuilds it nor closes its request, and it says whose it is.
Reports
Security reports go to the address in the repository's SECURITY.md, not as an issue.