Skip to content
Merged
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
62 changes: 50 additions & 12 deletions index.html

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@playwright/test": "1.61.1",
"acorn": "8.17.0",
"esbuild": "0.28.1",
"snappyjs": "0.7.0",
"sql.js": "1.14.1"
}
}
24 changes: 24 additions & 0 deletions sbom.spdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,25 @@
}
]
},
{
"name": "snappyjs",
"SPDXID": "SPDXRef-Package-snappyjs-0.7.0",
"versionInfo": "0.7.0",
"downloadLocation": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.7.0.tgz",
"filesAnalyzed": false,
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"copyrightText": "NOASSERTION",
"primaryPackagePurpose": "LIBRARY",
"comment": "Development or release dependency from package-lock.json.",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/snappyjs@0.7.0"
}
]
},
{
"name": "sql.js",
"SPDXID": "SPDXRef-Package-sql.js-1.14.1",
Expand Down Expand Up @@ -1231,6 +1250,11 @@
"relationshipType": "DEV_DEPENDENCY_OF",
"relatedSpdxElement": "SPDXRef-Package-hSQLite-Editor"
},
{
"spdxElementId": "SPDXRef-Package-snappyjs-0.7.0",
"relationshipType": "DEV_DEPENDENCY_OF",
"relatedSpdxElement": "SPDXRef-Package-hSQLite-Editor"
},
{
"spdxElementId": "SPDXRef-Package-hSQLite-Editor",
"relationshipType": "DEPENDS_ON",
Expand Down
110 changes: 110 additions & 0 deletions scripts/contract-tests/github-controls.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import { spawnSync } from "node:child_process";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
decodeAttestationBundle,
evaluateGithubControls,
hydrateAttestationResponse,
isTrustedAttestationBundleUrl,
renderPolicyTemplate,
selectGithubToken,
VERDICTS
} from "../validate-github-controls.mjs";
import SnappyJS from "snappyjs";

const policy = {
repository: "helbertm/hSQLite-Editor",
Expand Down Expand Up @@ -251,6 +255,112 @@ test("prefers GH_TOKEN without exposing or rewriting credential values", () => {
assert.equal(selectGithubToken({}), "");
});

test("accepts only credential-free HTTPS attestation bundle URLs", () => {
assert.equal(
isTrustedAttestationBundleUrl("https://example.blob.core.windows.net/attestations/bundle.json.sn?sig=redacted"),
true
);
for (const unsafeUrl of [
"http://example.com/bundle.json",
"https://user:secret@example.com/bundle.json",
"file:///tmp/bundle.json",
"not-a-url"
]) {
assert.equal(isTrustedAttestationBundleUrl(unsafeUrl), false);
}
});

test("decodes bounded Snappy attestation bundles from the current GitHub API", () => {
const bundle = attestation("https://slsa.dev/provenance/v1").bundle;
const compressed = SnappyJS.compress(Buffer.from(JSON.stringify(bundle)));
const decoded = decodeAttestationBundle(compressed, "application/x-snappy");
assert.deepEqual(decoded, bundle);
assert.throws(
() => decodeAttestationBundle(Buffer.alloc((1024 * 1024) + 1), "application/x-snappy"),
/size is outside the accepted range/
);
});

test("hydrates bundle-url-only attestations without changing policy evaluation", async () => {
const responses = passingResponses();
const bundleUrl = "https://example.blob.core.windows.net/attestations/bundle.json.sn?sig=redacted";
const provenance = attestation("https://slsa.dev/provenance/v1");
const hydrated = await hydrateAttestationResponse(
{
status: 200,
body: { attestations: [{ repository_id: 42, bundle_url: bundleUrl }] }
},
async url => {
assert.equal(url, bundleUrl);
return { status: 200, body: provenance.bundle };
}
);
responses.attestations.provenance = hydrated;
const result = evaluateGithubControls({
policy,
version,
responses,
manualConfirmations: { pagesAdminBypassDisabled: true }
});
assert.equal(result.exitCode, 0);
});

test("treats unavailable bundle URLs as transport failures", async () => {
const response = await hydrateAttestationResponse(
{
status: 200,
body: {
attestations: [{
repository_id: 42,
bundle_url: "https://example.blob.core.windows.net/attestations/missing.json.sn"
}]
}
},
async () => ({ status: 503, body: null })
);
assert.equal(response.transportError, true);
const responses = passingResponses();
responses.attestations.provenance = response;
const result = evaluateGithubControls({
policy,
version,
responses,
manualConfirmations: { pagesAdminBypassDisabled: true }
});
assert.equal(result.exitCode, 3);
});

test("keeps predicate and digest checks strict after bundle URL hydration", async () => {
for (const bundle of [
attestation("https://spdx.dev/Document/v2.3").bundle,
attestation("https://slsa.dev/provenance/v1", `sha256:${"d".repeat(64)}`).bundle
]) {
const responses = passingResponses();
responses.attestations.provenance = await hydrateAttestationResponse(
{
status: 200,
body: {
attestations: [{
repository_id: 42,
bundle_url: "https://example.blob.core.windows.net/attestations/invalid.json.sn"
}]
}
},
async () => ({ status: 200, body: bundle })
);
const result = evaluateGithubControls({
policy,
version,
responses,
manualConfirmations: { pagesAdminBypassDisabled: true }
});
assert.equal(result.exitCode, 1);
assert.ok(result.findings.some(finding =>
finding.control === "provenance attestation" && finding.verdict === VERDICTS.FAIL
));
}
});

test("release policy templates reject unsafe version input", () => {
assert.equal(renderPolicyTemplate("hSQLite-Editor-v{version}.html", version), "hSQLite-Editor-v0.3.143.html");
for (const unsafeVersion of ["0.3.143/../../x", "0.3.143%2f..", "0.3.143<script>", "0.3.143\n"]) {
Expand Down
33 changes: 33 additions & 0 deletions scripts/contract-tests/help-feature-request.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const template = fs.readFileSync(path.join(rootDir, "src/index.template.html"), "utf8");
const localization = fs.readFileSync(path.join(rootDir, "src/capabilities/03-localization.js"), "utf8");
const featureForm = fs.readFileSync(path.join(rootDir, ".github/ISSUE_TEMPLATE/feature.yml"), "utf8");

test("Help links to the scoped GitHub Feature request form without local data", () => {
const link = template.match(/<a id="suggestImprovementLink"[^>]+>/)?.[0] || "";
const href = link.match(/href="([^"]+)"/)?.[1] || "";

assert.equal(href, "https://github.com/helbertm/hSQLite-Editor/issues/new?template=feature.yml");
assert.match(link, /target="_blank"/);
assert.match(link, /rel="noopener noreferrer"/);
assert.match(link, /data-i18n-aria-label="issues\.suggestTooltip"/);
assert.doesNotMatch(href, /(?:body|description|proposal|title)=/i);
});

test("Feature request action is localized in every supported locale", () => {
for (const label of ["Suggest improvement", "Sugerir melhoria", "Sugerir una mejora"]) {
assert.match(localization, new RegExp(`"issues\\.suggest": "${label}"`));
}
assert.equal((localization.match(/"issues\.suggestTooltip":/g) || []).length, 3);
});

test("GitHub feature issue form remains available", () => {
assert.match(featureForm, /^name: Feature request$/m);
assert.match(featureForm, /^title: "\[Feature\]: "$/m);
});
39 changes: 39 additions & 0 deletions scripts/validate-browser-quality.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const locales = [
blockedAnnouncement: "Virtual relationships between columns in the same table are not supported.",
populationTitle: "Populate table for QA", exportTitle: "Export result", historyTitle: "Query history",
favoritesTitle: "Favorite queries", closeTabTitle: "Close tab?", renameTabPrefix: "Rename tab", closeTabPrefix: "Close tab", missingTableCause: "a table or view name was not found",
helpTitle: "hSQLite Editor help", suggestImprovement: "Suggest improvement: open the GitHub Feature request form in a new tab",
helpPrefix: "https://learn.microsoft.com/en-us/", csvFilename: "sqlite_result.csv"
},
{
Expand All @@ -24,6 +25,7 @@ const locales = [
blockedAnnouncement: "Relacionamentos virtuais entre colunas da mesma tabela não são suportados.",
populationTitle: "Popular tabela para QA", exportTitle: "Exportar resultado", historyTitle: "Histórico de consultas",
favoritesTitle: "Consultas favoritas", closeTabTitle: "Fechar aba?", renameTabPrefix: "Renomear aba", closeTabPrefix: "Fechar aba", missingTableCause: "uma tabela ou view não foi encontrada",
helpTitle: "Ajuda do hSQLite Editor", suggestImprovement: "Sugerir melhoria: abrir o formulário Feature request do GitHub em uma nova aba",
helpPrefix: "https://learn.microsoft.com/pt-br/", csvFilename: "resultado_sqlite.csv"
},
{
Expand All @@ -35,6 +37,7 @@ const locales = [
blockedAnnouncement: "No se admiten relaciones virtuales entre columnas de la misma tabla.",
populationTitle: "Poblar tabla para QA", exportTitle: "Exportar resultado", historyTitle: "Historial de consultas",
favoritesTitle: "Consultas favoritas", closeTabTitle: "¿Cerrar pestaña?", renameTabPrefix: "Renombrar pestaña", closeTabPrefix: "Cerrar pestaña", missingTableCause: "no se encontró una tabla o vista",
helpTitle: "Ayuda de hSQLite Editor", suggestImprovement: "Sugerir una mejora: abrir el formulario Feature request de GitHub en una pestaña nueva",
helpPrefix: "https://learn.microsoft.com/es-es/", csvFilename: "resultado_sqlite.csv"
}
];
Expand Down Expand Up @@ -149,6 +152,41 @@ try {
`${violation.id}: ${violation.nodes.map(node => node.target.join(" ")).join(" | ")}`
)).join(", ")}`);

await page.locator("#helpBtn").click();
await page.locator("#helpModal").waitFor({ state: "visible" });
assert(await page.getByRole("dialog", { name: locale.helpTitle }).count() === 1, `${locale.tag}/${viewport.name}: Help dialog name is not localized.`);
const suggestImprovementLink = page.getByRole("link", { name: locale.suggestImprovement });
assert(await suggestImprovementLink.count() === 1, `${locale.tag}/${viewport.name}: Suggest improvement link is missing or ambiguously named.`);
assert(
await suggestImprovementLink.getAttribute("href") === "https://github.com/helbertm/hSQLite-Editor/issues/new?template=feature.yml",
`${locale.tag}/${viewport.name}: Suggest improvement does not target the scoped Feature request form.`
);
assert(await suggestImprovementLink.getAttribute("target") === "_blank", `${locale.tag}/${viewport.name}: Suggest improvement does not open in a new tab.`);
assert(await suggestImprovementLink.getAttribute("rel") === "noopener noreferrer", `${locale.tag}/${viewport.name}: Suggest improvement lacks safe new-tab isolation.`);

const helpAxe = await new AxeBuilder({ page })
.include("#helpModal")
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
.analyze();
assert(helpAxe.violations.length === 0, `${locale.tag}/${viewport.name}: Help violations: ${helpAxe.violations.map(violation => (
`${violation.id}: ${violation.nodes.map(node => node.target.join(" ")).join(" | ")}`
)).join(", ")}`);

const helpViewport = page.viewportSize();
await page.setViewportSize({ width: 320, height: helpViewport.height });
await page.waitForTimeout(50);
const helpReflow = await page.evaluate(() => ({
pageOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
modalOverflow: document.querySelector("#helpModal .modal")?.scrollWidth > document.querySelector("#helpModal .modal")?.clientWidth + 1
}));
assert(!helpReflow.pageOverflow, `${locale.tag}/${viewport.name}: Help causes horizontal page overflow at 320 CSS px.`);
assert(!helpReflow.modalOverflow, `${locale.tag}/${viewport.name}: Help content overflows its modal at 320 CSS px.`);
await page.setViewportSize(helpViewport);

await page.keyboard.press("Escape");
await page.locator("#helpModal").waitFor({ state: "hidden" });
assert(await page.locator("#helpBtn").evaluate(element => element === document.activeElement), `${locale.tag}/${viewport.name}: closing Help did not restore focus.`);

await page.evaluate(async () => {
const SqlJs = await initSqlJsIfNeeded();
const database = new SqlJs.Database();
Expand Down Expand Up @@ -397,6 +435,7 @@ try {
viewport: viewport.name,
shellViolations: axe.violations.length,
shellSerious: serious.length,
helpViolations: helpAxe.violations.length,
tabViolations: tabAxe.violations.length,
sqlMapViolations: sqlMapAxe.violations.length,
sqlMapSerious: sqlMapSerious.length
Expand Down
Loading