From 3cd2f914a7400175c3e7c7fd9b5202c9b195f644 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 31 Jul 2026 14:57:56 +0200 Subject: [PATCH 1/3] JS: Support MaD targeting specific files For codebase-specific models it's useful to be able to write models for specific files, without an NPM package boundary around it. But previously it was only possible to use NPM package exports as the starting point of a model. This adds the type `file:` which uses imports of the given file as the starting point, exactly as it if had been importing aname NPM package. --- .../data/internal/ApiGraphModelsSpecific.qll | 49 +++++++++++++++++++ .../frameworks/data/foo/bar/baz.js | 1 + .../frameworks/data/importFileBasedModel.js | 5 ++ .../frameworks/data/test.expected | 1 + .../frameworks/data/test.ext.yml | 1 + 5 files changed, 57 insertions(+) create mode 100644 javascript/ql/test/library-tests/frameworks/data/foo/bar/baz.js create mode 100644 javascript/ql/test/library-tests/frameworks/data/importFileBasedModel.js diff --git a/javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll b/javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll index 00929f19d279..837aa6463b35 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll @@ -53,6 +53,13 @@ predicate parseTypeString(string rawType, string package, string qualifiedName) qualifiedName = "" } +/** If `type` has the form `file:` gets the path to the file. */ +bindingset[type] +overlay[caller] +private string getRawFilePathFromTypeName(string type) { + result = type.regexpCapture("file:(.*)", 1) +} + /** * Holds if models describing `package` may be relevant for the analysis of this database. */ @@ -76,6 +83,8 @@ predicate isTypeUsed(string type) { parseTypeString(type, package, _) and isPackageUsed(package) ) + or + exists(getRawFilePathFromTypeName(type)) // No need to prune repository-specific models } /** @@ -126,6 +135,41 @@ private API::Node getGlobalNode(string globalName) { result = any(GlobalApiEntryPoint e | e.getGlobal() = globalName).getANode() } +/** Holds if `type` is used as a type string in a model, and has the form `file:` */ +overlay[local] +private predicate relevantRawFilePath(string type, string filePath) { + isRelevantType(type) and + filePath = getRawFilePathFromTypeName(type) +} + +/** An API graph entry point for package specifiers of form `file:`. */ +overlay[local?] +private class RawFilePathEntryPoint extends API::EntryPoint { + string path; + + RawFilePathEntryPoint() { + relevantRawFilePath(_, path) and + this = "RawFilePathEntryPoint:" + path + } + + override DataFlow::SourceNode getASource() { + exists(JS::Import imprt | + imprt.getImportedFile().getRelativePath() = path and + result = imprt.getImportedModuleNode() + ) + } + + /** Gets the name of the path variable. */ + string getPath() { result = path } +} + +/** + * Gets an API node referring to the given global variable (if relevant). + */ +private API::Node getRawFilePathNode(string rawFilePathNode) { + result = any(RawFilePathEntryPoint e | e.getPath() = rawFilePathNode).getANode() +} + /** Gets a JavaScript-specific interpretation of the `(type, path)` tuple after resolving the first `n` access path tokens. */ bindingset[type, path] API::Node getExtraNodeFromPath(string type, AccessPath path, int n) { @@ -150,6 +194,11 @@ API::Node getExtraNodeFromType(string type) { // Access instance of a type based on type annotations result = API::Internal::getANodeOfTypeRaw(package, qualifiedName) ) + or + exists(string filePath | + relevantRawFilePath(type, filePath) and + result = getRawFilePathNode(filePath) + ) } /** diff --git a/javascript/ql/test/library-tests/frameworks/data/foo/bar/baz.js b/javascript/ql/test/library-tests/frameworks/data/foo/bar/baz.js new file mode 100644 index 000000000000..bb1843d113a5 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/data/foo/bar/baz.js @@ -0,0 +1 @@ +export const foo = 1; diff --git a/javascript/ql/test/library-tests/frameworks/data/importFileBasedModel.js b/javascript/ql/test/library-tests/frameworks/data/importFileBasedModel.js new file mode 100644 index 000000000000..4fcb5c3dc1cf --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/data/importFileBasedModel.js @@ -0,0 +1,5 @@ +import * as bar from './foo/bar/baz'; + +function t1() { + sink(bar.customSource()); // NOT OK +} diff --git a/javascript/ql/test/library-tests/frameworks/data/test.expected b/javascript/ql/test/library-tests/frameworks/data/test.expected index 0bc1b6b6ee07..0f945b6c3515 100644 --- a/javascript/ql/test/library-tests/frameworks/data/test.expected +++ b/javascript/ql/test/library-tests/frameworks/data/test.expected @@ -5,6 +5,7 @@ taintFlow | guardedRouteHandler.js:10:10:10:28 | res.injectedResData | guardedRouteHandler.js:10:10:10:28 | res.injectedResData | | guardedRouteHandler.js:16:10:16:28 | req.injectedReqData | guardedRouteHandler.js:16:10:16:28 | req.injectedReqData | | guardedRouteHandler.js:20:10:20:28 | res.injectedResData | guardedRouteHandler.js:20:10:20:28 | res.injectedResData | +| importFileBasedModel.js:4:10:4:27 | bar.customSource() | importFileBasedModel.js:4:10:4:27 | bar.customSource() | | paramDecorator.ts:6:54:6:54 | x | paramDecorator.ts:7:10:7:10 | x | | test.js:5:30:5:37 | source() | test.js:5:8:5:38 | testlib ... urce()) | | test.js:6:22:6:29 | source() | test.js:6:8:6:30 | preserv ... urce()) | diff --git a/javascript/ql/test/library-tests/frameworks/data/test.ext.yml b/javascript/ql/test/library-tests/frameworks/data/test.ext.yml index 1ac621936a4a..868a8d18998c 100644 --- a/javascript/ql/test/library-tests/frameworks/data/test.ext.yml +++ b/javascript/ql/test/library-tests/frameworks/data/test.ext.yml @@ -15,6 +15,7 @@ extensions: - ['danger-constant', 'Member[danger]', 'test-source'] - ['testlib', 'Member[middleware].ReturnValue.GuardedRouteHandler.Parameter[0].Member[injectedReqData]', 'test-source'] - ['testlib', 'Member[middleware].ReturnValue.GuardedRouteHandler.Parameter[1].Member[injectedResData]', 'test-source'] + - ['file:foo/bar/baz.js', 'Member[customSource].ReturnValue', 'test-source'] - addsTo: pack: codeql/javascript-all From 8f387b741da9ff1d338cde8f61695c3da6a2b839 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 31 Jul 2026 15:02:21 +0200 Subject: [PATCH 2/3] JS: Add documentation --- .../customizing-library-models-for-javascript.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/codeql/codeql-language-guides/customizing-library-models-for-javascript.rst b/docs/codeql/codeql-language-guides/customizing-library-models-for-javascript.rst index 77ea678f5468..7ede0d0aff3a 100644 --- a/docs/codeql/codeql-language-guides/customizing-library-models-for-javascript.rst +++ b/docs/codeql/codeql-language-guides/customizing-library-models-for-javascript.rst @@ -549,6 +549,7 @@ A type can be defined by adding ``typeModel`` tuples for that type. Additionally - The name of an NPM package matches imports of that package. For example, the type ``express`` matches the expression ``require("express")``. If the package name includes dots, it must be surrounded by single quotes, such as in ``'lodash.escape'``. - The type ``global`` identifies the global object, also known as ``window``. In JavaScript, global variables are properties of the global object, so global variables can be identified using this type. (This type also matches imports of the NPM package named ``global``, which is a package that happens to export the global object.) - A qualified type name of form ``.`` identifies expressions of type ```` from ````. For example, ``mysql.Connection`` identifies expression of type ``Connection`` from the ``mysql`` package. Note that this only works if type annotations are present in the codebase, or if sufficient ``typeModel`` tuples have been provided for that type. +- A string of form ``file:`` identifies expressions that are imported from a file at the given path. The path is relative to the root of the codebase, must use forward slashes as path separator, must include the file extension, and is case-sensitive. For example, ``file:src/utils.js`` identifies expressions such as ``require('./utils')`` inside ``src/``, or ``require('../src/utils')`` inside another top-level folder. Access paths ------------ From d0cb4ef80212f6b51a17bd0701f888061221b510 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 31 Jul 2026 15:07:54 +0200 Subject: [PATCH 3/3] JS: Add change note --- .../ql/lib/change-notes/2026-07-31-file-scoped-models.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-07-31-file-scoped-models.md diff --git a/javascript/ql/lib/change-notes/2026-07-31-file-scoped-models.md b/javascript/ql/lib/change-notes/2026-07-31-file-scoped-models.md new file mode 100644 index 000000000000..b3df112a1f1d --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-07-31-file-scoped-models.md @@ -0,0 +1,6 @@ +--- +category: majorAnalysis +--- +* It is now possible for custom models to refer to specific files in the codebase, using a package name of form `file:`. The model should describe the public exports + of that file. This can be used to derive sources and sinks in code that imports the file, but note that sources and sinks will not generally be placed within the file itself. + For example, a source model `['file:lib/service.js', 'Member[getData].ReturnValue', 'remote']` could identify `require('../lib/service').getData()` as a source.