mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
search-with-proof: reproducible npm builds for old lockless projects (--before commit date)
A lockless npm project (e.g. glance, 2015) resolved an unpinned transitive range to the latest major ([email protected] -> mime@4, which dropped .lookup()), so the built app crashed serving files -- a build-reproducibility defect, not an app or agent fault. The executor now, for the npm-install (no-lock) case only and only when .git is present, resolves dependencies as of the checkout's HEAD commit date via `npm install --before <date>`, giving era-appropriate, compatible versions. npm ci (lockfile present) is untouched, and any git/date failure falls back to plain install. demo.py also recognizes npm-shrinkwrap.json as a lock for the ci-vs-install choice. Tests: executor + smoke 75 passed, demo 107 passed; ruff clean; mypy clean on executor.py. Executor-side change -> image rebuild required. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8de0c2732b
commit
c30dd9126a
@@ -270,7 +270,8 @@ def preparation_command(recipe: str, path: Path, checkout: Path, products: Path)
|
|||||||
*(["-r", str(target)] if requirements else [str(target)]),
|
*(["-r", str(target)] if requirements else [str(target)]),
|
||||||
], project
|
], project
|
||||||
if recipe == "npm":
|
if recipe == "npm":
|
||||||
return ["npm", "ci" if (project / "package-lock.json").is_file() else "install", "--ignore-scripts"], project
|
locked = any((project / name).is_file() for name in ("package-lock.json", "npm-shrinkwrap.json"))
|
||||||
|
return ["npm", "ci" if locked else "install", "--ignore-scripts"], project
|
||||||
if recipe == "composer":
|
if recipe == "composer":
|
||||||
return ["composer", "install", "--no-interaction", "--no-plugins", "--no-scripts"], project
|
return ["composer", "install", "--no-interaction", "--no-plugins", "--no-scripts"], project
|
||||||
if recipe == "maven":
|
if recipe == "maven":
|
||||||
|
|||||||
@@ -412,6 +412,20 @@ class InProcessExecutor:
|
|||||||
if recipe["recipe"] != "none":
|
if recipe["recipe"] != "none":
|
||||||
path = self._path(recipe["arguments"][0])
|
path = self._path(recipe["arguments"][0])
|
||||||
argv, project = preparation_command(recipe["recipe"], path, self.repo, products)
|
argv, project = preparation_command(recipe["recipe"], path, self.repo, products)
|
||||||
|
if argv[:2] == ["npm", "install"] and (self.repo / ".git").exists():
|
||||||
|
# Lockless old projects should resolve dependencies from their era.
|
||||||
|
# Metadata is optional; unavailable git must not prevent a build.
|
||||||
|
with suppress(OSError, subprocess.SubprocessError, UnicodeError):
|
||||||
|
commit_date = subprocess.run(
|
||||||
|
["git", "-C", str(project), "show", "-s", "--format=%cI", "HEAD"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=clean_environment(),
|
||||||
|
timeout=min(5, self._remaining()),
|
||||||
|
).stdout.strip()
|
||||||
|
if commit_date:
|
||||||
|
argv.extend(["--before", commit_date])
|
||||||
self.phase = "build"
|
self.phase = "build"
|
||||||
built = self._task_run(argv, self._remaining(), {"disk_bytes": task_storage.DEFAULT_SCRATCH_BYTES, **limits}, cwd=project)
|
built = self._task_run(argv, self._remaining(), {"disk_bytes": task_storage.DEFAULT_SCRATCH_BYTES, **limits}, cwd=project)
|
||||||
phases["build"] = time.monotonic() - before
|
phases["build"] = time.monotonic() - before
|
||||||
|
|||||||
@@ -1,12 +1,74 @@
|
|||||||
"""Offline executor boundary and mailbox protocol checks."""
|
"""Offline executor boundary and mailbox protocol checks."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from openultrasast.search.executor import MAX_RESULT_BYTES, Command, InProcessExecutor, ObjectStoreExecutor, clean_environment, serve
|
from openultrasast.search.executor import MAX_RESULT_BYTES, Command, InProcessExecutor, ObjectStoreExecutor, clean_environment, serve
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("lockfile", [None, "package-lock.json", "npm-shrinkwrap.json"])
|
||||||
|
@pytest.mark.parametrize("git_date", ["ok", "no_git", "missing", "failed", "empty", "timeout"])
|
||||||
|
def test_npm_preparation_commit_date(tmp_path, monkeypatch, lockfile, git_date):
|
||||||
|
from openultrasast.search import executor
|
||||||
|
|
||||||
|
# A nested project must still use the checkout's HEAD, with its own build cwd.
|
||||||
|
project = tmp_path / "app"
|
||||||
|
project.mkdir()
|
||||||
|
(project / "package.json").write_text("{}")
|
||||||
|
(project / "index.js").write_text('console.log("ready")')
|
||||||
|
if lockfile:
|
||||||
|
(project / lockfile).write_text("{}")
|
||||||
|
if git_date != "no_git":
|
||||||
|
(tmp_path / ".git").mkdir()
|
||||||
|
date = "2015-06-03T12:34:56+02:00"
|
||||||
|
git_calls, builds = [], []
|
||||||
|
|
||||||
|
def git(argv, **kwargs):
|
||||||
|
git_calls.append(argv)
|
||||||
|
assert argv == ["git", "-C", str(project), "show", "-s", "--format=%cI", "HEAD"]
|
||||||
|
assert kwargs["check"] and 0 < kwargs["timeout"] <= 5
|
||||||
|
assert kwargs["env"] == clean_environment()
|
||||||
|
if git_date == "missing":
|
||||||
|
raise FileNotFoundError("git")
|
||||||
|
if git_date == "failed":
|
||||||
|
raise subprocess.CalledProcessError(128, argv)
|
||||||
|
if git_date == "timeout":
|
||||||
|
raise subprocess.TimeoutExpired(argv, kwargs["timeout"])
|
||||||
|
return subprocess.CompletedProcess(argv, 0, " \n" if git_date == "empty" else date + "\n", "")
|
||||||
|
|
||||||
|
def build(argv, timeout, limits, *, cwd):
|
||||||
|
builds.append(argv)
|
||||||
|
assert cwd == project
|
||||||
|
return {"exit_code": 0, "timed_out": False, "stderr": ""}
|
||||||
|
|
||||||
|
app = InProcessExecutor(tmp_path, task_boundary=True)
|
||||||
|
app.prepared_put_url = "https://files.example/output"
|
||||||
|
monkeypatch.setattr(executor.subprocess, "run", git)
|
||||||
|
monkeypatch.setattr(app, "_task_run", build)
|
||||||
|
monkeypatch.setattr(executor.URLTransport, "put", lambda *args: None)
|
||||||
|
result = app.submit(
|
||||||
|
"prepare_verification",
|
||||||
|
{
|
||||||
|
"demo": {
|
||||||
|
"oracle": "path",
|
||||||
|
"build": {"recipe": "npm", "arguments": ["app/package.json"]},
|
||||||
|
"start": {"runtime": "node", "path": "app/index.js", "arguments": [], "mode": "cli"},
|
||||||
|
"steps": [{"type": "cli", "arguments": []}],
|
||||||
|
},
|
||||||
|
"family": "path",
|
||||||
|
"timeout_seconds": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["status"] == "ok", result
|
||||||
|
expected = ["npm", "ci" if lockfile else "install", "--ignore-scripts"]
|
||||||
|
if not lockfile and git_date == "ok":
|
||||||
|
expected += ["--before", date]
|
||||||
|
assert builds == [expected]
|
||||||
|
assert len(git_calls) == int(not lockfile and git_date != "no_git")
|
||||||
|
|
||||||
|
|
||||||
def test_order_retry_stop(tmp_path):
|
def test_order_retry_stop(tmp_path):
|
||||||
(tmp_path / "input").write_text("hello")
|
(tmp_path / "input").write_text("hello")
|
||||||
executor = InProcessExecutor(tmp_path)
|
executor = InProcessExecutor(tmp_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user