This repository builds a public web page from a private data source:
- Secrets held by the production GitHub Actions workflow
(
.github/workflows/main.yml) -- the Google service-account JSON passed in asGSHEET_CREDSand the sheet identifierGSHEET_ID. Both come from repository secrets and are scoped to the deploy job. - Absolute portfolio values read from that Google Sheet -- share counts, per-trade sizes, cash balances, dividend cash payouts, and above all the total portfolio value (net worth). None of these are intended to be published. The page exposes derived percentages (weights, returns, allocations) plus per-share transaction prices (market-observable, and disclosing nothing about position size on their own). The invariant: no published value, alone or combined with others, may reveal net worth -- or a share count / position value / cash balance from which it could be derived -- at any point in time.
Because the build runs in a public repository, job logs are world-readable and act as a side channel for both classes of data.
investing.safe_run._run_main_safelywrapsmain()so that bothsys.stderr/ fd 2 andsys.stdout/ fd 1 are fully redirected for the duration of the build. The stderr path silences output from C extensions that bypass the Python wrappers (NumPy / Pandas warnings,gspreadHTTP error bodies,yfinancerate-limit notices, etc.). The stdout path defends against a transitive dependency emitting nominal values viaprint()(debug modes inhttpx,tqdm-style progress lines, etc.) -- those land in a discarded in-memory buffer rather than the public job log.- The curated
Build OK: ...summary line routes throughinvesting.safe_run.emit_summary, which writes to the stashed real stdout while the redaction is active. The line is composed exclusively of quantities the rendered page also publishes (TWR / CAGR percentages, holding counts) so the job log gets a positive build signal without surfacing privacy-sensitive values. - On failure, the wrapper restores
stderrand prints a hand-formatted traceback that is deliberately built from public identifiers only: exception class, per-framefilename:lineno, function name, source line. It dropsstr(exc), exception__notes__and local variables -- the usual channels through which runtime values surface. - All progress / diagnostic output in the pipeline goes through
investing.log.logger; the package contains noprint()calls in non-test code (other than the curated summary above). Do not reintroduce them. _gspread_clientaccepts the service-account JSON inline via theGSHEET_CREDSenvironment variable so the secret never lands on the runner's filesystem.- The deploy workflow uses the least-privilege token scope (
contents: read,pages: write,id-token: write). .github/workflows/security.ymlrunspip-auditagainst both lockfiles (OSV.dev advisory feed,--strictso known CVEs fail the build) and a CodeQL Python scan (security-and-qualityquery set) on every push, PR, and weekly cron sweep.
The rendered page loads exactly one third-party asset: the Cloudflare
Web Analytics beacon
(https://static.cloudflareinsights.com/beacon.min.js). It is
allow-listed in the page's script-src Content-Security-Policy
directive (investing/webpage/head.py) and the matching <script>
tag is composed by investing.webpage.head.build_analytics_tag.
The embedded data-cf-beacon token is a write-only identifier
(grants push access to the analytics dashboard but reveals nothing
about the dataset on its own), so its presence in source is
intentional. Removing or swapping the provider means editing both
the CSP and the tag together; the matching head.py constants are
the single edit surface.
Please open a private security advisory via GitHub's "Security" tab rather than a public issue or PR. If you believe a leak has already occurred in published job logs or in the served page, include the specific URL and the line range so we can rotate the affected secrets and rewrite history if needed.
If you touch investing/safe_run.py, the workflow files, or anything
that runs around main(), please verify:
- No new
print()calls reachingsys.stdoutdirectly (useinvesting.log.loggerfor diagnostics; route the curated build signal throughsafe_run.emit_summary). The leak-safe wrapper now redirects stdout too, but a future redaction that misses one of the streams would silently re-open the leak. - No new redirections that swap stderr / stdout back to a tty path.
- No code paths that re-raise with the offending value embedded in
the message (e.g.
raise ValueError(f"bad row: {row!r}")). Use theSheetParseErrorpattern ininvesting/sheets.pyinstead -- it builds the message from row coordinates only. - No
--no-redactstyle escape hatch in CI; if a build fails and the sanitized summary isn't enough, reproduce locally withpython -m investinginstead.