Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
826 changes: 413 additions & 413 deletions .github/actions/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/bug-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Add Comment
uses: ./.github/actions/AddComment
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/by-design-closer-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/by-design-closer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/duplicate-closer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/enhancement-closer-no-milestone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/enhancement-closer-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/enhancement-reopener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Run Reopener
uses: ./.github/actions/Reopener
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/external-closer-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/feature-request-closer-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/feature-request-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Add Comment
uses: ./.github/actions/AddComment
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/feature-request-reopener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Run Reopener
uses: ./.github/actions/Reopener
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/investigate-closer-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/job-compile-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
with:
node-version: 24

- name: Validate Yarn lockfile
run: yarn test-yarn-lock && yarn verify-yarn-lock
- name: Validate dependency lockfiles
run: yarn test-lockfiles && yarn verify-lockfiles
working-directory: Extension

- name: Install Dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/locker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Run Locker
uses: ./.github/actions/Locker
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/more-info-needed-closer-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/more-info-needed-closer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/question-closer-debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/question-closer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Checkout Actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Actions
run: cd ./.github/actions && npm install --production && cd ../..
run: cd ./.github/actions && npm ci --omit=dev && cd ../..
- name: Stale Closer
uses: ./.github/actions/StaleCloser
with:
Expand Down
4 changes: 2 additions & 2 deletions Build/package/jobs_package_vsix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ jobs:
script: |
export SRC_DIR=$(echo $BUILD_SOURCESDIRECTORY | sed 's|\\|/|g')
cd "$SRC_DIR/${{ parameters.srcDir }}"
npm install
npm ci
if [ $? -ne 0 ]; then
echo "npm install failed, sleeping for 30s before retrying..."
echo "npm ci failed, sleeping for 30s before retrying..."
sleep 30
exit 1
fi
Expand Down
92 changes: 92 additions & 0 deletions Extension/.scripts/packageLockFiles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'node:fs';
import path from 'node:path';
import { hasLockedIntegrity } from './subresourceIntegrity.mjs';

const excludedDirectoryNames = new Set(['.git', 'node_modules']);

function findPackageLockPaths(repositoryRoot) {
const packageLockPaths = [];

function visit(directory) {
const entries = fs.readdirSync(directory, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name));
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory() && !excludedDirectoryNames.has(entry.name)) {
visit(entryPath);
} else if (entry.isFile() && entry.name === 'package-lock.json') {
packageLockPaths.push(entryPath);
}
}
}

visit(repositoryRoot);
return packageLockPaths;
}

function getWorkspaceTargetPath(packageEntry) {
if (packageEntry.link !== true || typeof packageEntry.resolved !== 'string') {
return undefined;
}

const workspacePath = packageEntry.resolved.replaceAll('\\', '/');
const normalizedPath = path.posix.normalize(workspacePath);
return workspacePath.length > 0
&& !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(workspacePath)
&& !path.posix.isAbsolute(workspacePath)
&& normalizedPath === workspacePath
&& normalizedPath !== '.'
&& normalizedPath !== '..'
&& !normalizedPath.startsWith('../')
? normalizedPath
: undefined;
}

function getWorkspacePaths(packages) {
return new Set(Object.values(packages)
.map(getWorkspaceTargetPath)
.filter(workspacePath => workspacePath !== undefined)
.filter(workspacePath => {
const workspaceEntry = packages[workspacePath];
return !workspacePath.split('/').includes('node_modules')
&& workspaceEntry !== undefined
&& workspaceEntry.resolved === undefined
&& workspaceEntry.integrity === undefined;
}));
}

function isBundledPackageEntry(packages, packagePath, packageEntry) {
if (packageEntry.inBundle !== true || !packagePath.includes('/node_modules/')) {
return false;
}

let ancestorPath = packagePath.slice(0, packagePath.lastIndexOf('/node_modules/'));
while (ancestorPath) {
const ancestorEntry = packages[ancestorPath];
if (ancestorEntry && hasLockedIntegrity(ancestorEntry.integrity)) {
const relativePath = packagePath.slice(`${ancestorPath}/node_modules/`.length);
const relativeSegments = relativePath.split('/');
const packageName = relativeSegments[0].startsWith('@')
? relativeSegments.slice(0, 2).join('/')
: relativeSegments[0];
return Array.isArray(ancestorEntry.bundleDependencies) && ancestorEntry.bundleDependencies.includes(packageName);
}

const nextSeparator = ancestorPath.lastIndexOf('/node_modules/');
if (nextSeparator === -1) {
break;
}
ancestorPath = ancestorPath.slice(0, nextSeparator);
}
return false;
}

function isExplicitLocalPackageEntry(packages, workspacePaths, packagePath, packageEntry) {
const normalizedPackagePath = packagePath.replaceAll('\\', '/');
return workspacePaths.has(getWorkspaceTargetPath(packageEntry))
|| workspacePaths.has(normalizedPackagePath)
|| (packageEntry.link !== true && typeof packageEntry.resolved === 'string' && /^(?:file|link|workspace):/i.test(packageEntry.resolved))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent102): [Minor] This exemption treats a lockfile-controlled resolved prefix as authorization to skip integrity, without validating the target — and the same pattern exists on the Yarn side at verifyYarnLock.mjs line 93 (!isExplicitLocalResolution(currentEntry.resolved)). Both were raised as suppressed low-confidence observations in the automated review at this head; I verified them against this revision rather than taking them as given.

Calling the exported validators directly at 62cd4633:

Fixture (no integrity) Result
npm node_modules/evil with resolved: "file:../../outside/evil.tgz" accepted
npm node_modules/evil with resolved: "link:/etc/evil" accepted
npm node_modules/evil with resolved: "workspace:../../outside" accepted
npm node_modules/evil with link: true, resolved: "../../outside" rejected
npm node_modules/evil with resolved: "https://example.test/evil.tgz" rejected
yarn dependency@1.0.0 with resolved "file:../../outside/dep.tgz" accepted
yarn dependency@1.0.0 with resolved "link:/etc/evil" accepted
yarn dependency@1.0.0 with resolved "https://example.test/dep.tgz" rejected

So the link: true path that was hardened earlier in this PR rejects traversal, absolute, and protocol targets, while its non-link sibling accepts an arbitrary string after the protocol prefix. On the Yarn side an ordinary registry selector is exempted purely because of its resolved line, which does not match the earlier statement that "only selectors whose actual dependency range uses an explicit file:, link:, or workspace: protocol are exempt" — the condition is an OR of the selector check and the resolution check.

Impact is bounded: no entry in any current lockfile lacks integrity, so nothing relies on this exemption today, and it only matters for a future lockfile change. But since the stated goal is repository-wide integrity enforcement, it is worth closing so the validator cannot be opted out of by editing the lockfile it is validating.

Minimal resolution: apply the same target rules the link: true path already uses — resolve the local target relative to the lockfile and exempt only normalized, repository-contained paths — or, for Yarn, rely on hasExplicitLocalSelector alone. Because every current entry carries integrity, either change is inert against the lockfiles in this PR.

|| isBundledPackageEntry(packages, normalizedPackagePath, packageEntry);
}

export { findPackageLockPaths, getWorkspacePaths, isExplicitLocalPackageEntry };
52 changes: 52 additions & 0 deletions Extension/.scripts/subresourceIntegrity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';

const digestLengths = new Map([
['sha1', 20],
['sha256', 32],
['sha384', 48],
['sha512', 64]
]);
const supportedAlgorithms = new Set(['sha256', 'sha384', 'sha512']);

function calculateIntegrity(algorithm, content) {
return `${algorithm}-${createHash(algorithm).update(content).digest('base64')}`;
}

function parseValidDigest(digest) {
const metadataSeparator = digest.indexOf('?');
const digestWithoutMetadata = metadataSeparator === -1 ? digest : digest.slice(0, metadataSeparator);
const match = /^(sha1|sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(digestWithoutMetadata);
if (!match) {
return undefined;
}

const [, algorithm, serializedDigest] = match;
const decodedDigest = Buffer.from(serializedDigest, 'base64');
return decodedDigest.length === digestLengths.get(algorithm)
&& decodedDigest.toString('base64').replace(/=+$/, '') === serializedDigest.replace(/=+$/, '')
? { algorithm, digest: decodedDigest }
: undefined;
}

function hasLockedIntegrity(integrity) {
return typeof integrity === 'string' && integrity.split(/\s+/).some(digest => parseValidDigest(digest) !== undefined);
}

function hasSupportedIntegrityAlgorithm(integrity) {
return typeof integrity === 'string' && integrity.split(/\s+/).some(digest => supportedAlgorithms.has(parseValidDigest(digest)?.algorithm));
}

function integrityMatchesContent(integrity, algorithm, content) {
if (typeof integrity !== 'string') {
return false;
}

const expectedDigest = createHash(algorithm).update(content).digest();
return integrity.split(/\s+/).some(serializedDigest => {
const parsedDigest = parseValidDigest(serializedDigest);
return parsedDigest?.algorithm === algorithm && parsedDigest.digest.equals(expectedDigest);
});
}

export { calculateIntegrity, hasLockedIntegrity, hasSupportedIntegrityAlgorithm, integrityMatchesContent };
Loading
Loading