Dynamic/JS: Add support for re-exporting type models

This commit is contained in:
Asger F
2024-04-04 15:06:19 +02:00
parent acef9b7111
commit c55e03c588
5 changed files with 138 additions and 2 deletions

View File

@@ -72,6 +72,13 @@ signature module ModelExportSig {
* Holds if a named must be generated for `node` if it is to be included in the exported graph.
*/
default predicate mustBeNamed(API::Node node) { none() }
/**
* Holds if the exported model should preserve all paths leading to an instance of `type`,
* including partial ones. It does not need to be closed transitively, `ModelExport` will
* extend this to include type models from which `type` can be derived.
*/
default predicate shouldContainType(string type) { none() }
}
/**
@@ -79,6 +86,7 @@ signature module ModelExportSig {
*/
module ModelExport<ModelExportSig S> {
private import codeql.mad.dynamic.GraphExport
private import internal.ApiGraphModelsExport
private module GraphExportConfig implements GraphExportSig<API::Node> {
predicate edge = Specific::apiGraphHasEdge/3;
@@ -147,7 +155,7 @@ module ModelExport<ModelExportSig S> {
}
}
private module ExportedGraph = GraphExport<API::Node, GraphExportConfig>;
private module ExportedGraph = TypeGraphExport<GraphExportConfig, S::shouldContainType/1>;
import ExportedGraph
}

View File

@@ -267,7 +267,7 @@ private predicate summaryModel(string type, string path, string input, string ou
}
/** Holds if a type model exists for the given parameters. */
private predicate typeModel(string type1, string type2, string path) {
predicate typeModel(string type1, string type2, string path) {
exists(string row |
typeModel(row) and
row.splitAt(";", 0) = type1 and

View File

@@ -0,0 +1,122 @@
/**
* Contains an extension of `GraphExport` that relies on API graph specific functionality.
*/
private import ApiGraphModels as Shared
private import codeql.mad.dynamic.GraphExport
private import ApiGraphModelsSpecific as Specific
private module API = Specific::API;
private import Shared
/**
* Holds if some proper prefix of `(type, path)` evaluated to `node`, where `remainingPath`
* is bound to the suffix of `path` that was not evaluated yet.
*/
bindingset[type, path]
predicate partiallyEvaluatedModel(string type, string path, API::Node node, string remainingPath) {
exists(int n, AccessPath accessPath |
accessPath = path and
getNodeFromPath(type, accessPath, n) = node and
n > 0 and
// Note that `n < accessPath.getNumToken()` is implied by the use of strictconcat()
remainingPath =
strictconcat(int k |
k = [n .. accessPath.getNumToken() - 1]
|
accessPath.getToken(k), "." order by k
)
)
}
/**
* Holds if `type` and all types leading to `type` should be re-exported.
*/
signature predicate shouldContainTypeSig(string type);
/**
* Wrapper around `GraphExport` that also exports information about re-exported types.
*
* ### JavaScript example 1
* For example, suppose `shouldContainType("foo")` holds, and the following is the entry point for a package `bar`:
* ```js
* // bar.js
* module.exports.xxx = require('foo');
* ```
* then this would generate the following type model:
* ```
* foo; bar; Member[xxx]
* ```
*
* ### JavaScript example 2
* For a more complex case, suppose the following type model exists:
* ```
* foo.XYZ; foo; Member[x].Member[y].Member[z]
* ```
* And the package exports something that matches a prefix of the access path above:
* ```js
* module.exports.blah = require('foo').x.y;
* ```
* This would result in the following type model:
* ```
* foo.XYZ; bar; Member[blah].Member[z]
* ```
* Notice that the access path `Member[blah].Member[z]` consists of an access path generated from the API
* graph, with pieces of the access path from the original type model appended to it.
*/
module TypeGraphExport<GraphExportSig<API::Node> S, shouldContainTypeSig/1 shouldContainType> {
/** Like `shouldContainType` but includes types that lead to `type` via type models. */
private predicate shouldContainTypeEx(string type) {
shouldContainType(type)
or
exists(string prevType |
shouldContainType(prevType) and
Shared::typeModel(prevType, type, _)
)
}
private module Config implements GraphExportSig<API::Node> {
import S
predicate shouldContain(API::Node node) {
S::shouldContain(node)
or
exists(string type1 | shouldContainTypeEx(type1) |
ModelOutput::getATypeNode(type1).getAValueReachableFromSource() = node.asSink()
or
exists(string type2, string path |
Shared::typeModel(type1, type2, path) and
getNodeFromPath(type2, path, _).getAValueReachableFromSource() = node.asSink()
)
)
}
}
private module ExportedGraph = GraphExport<API::Node, Config>;
import ExportedGraph
/**
* Holds if `type1, type2, path` should be emitted as a type model, that is `(type2, path)` leads to an instance of `type1`.
*/
predicate typeModel(string type1, string type2, string path) {
ExportedGraph::typeModel(type1, type2, path)
or
shouldContainTypeEx(type1) and
exists(API::Node node |
// A relevant type is exported directly
ModelOutput::getATypeNode(type1).getAValueReachableFromSource() = node.asSink() and
ExportedGraph::pathToNode(type2, path, node)
or
// Something that leads to a relevant type, but didn't finish its access path, is exported
exists(string midType, string midPath, string remainingPath, string prefix, API::Node source |
Shared::typeModel(type1, midType, midPath) and
partiallyEvaluatedModel(midType, midPath, source, remainingPath) and
source.getAValueReachableFromSource() = node.asSink() and
ExportedGraph::pathToNode(type2, prefix, node) and
path = join(prefix, remainingPath)
)
)
}
}

View File

@@ -1,3 +1,7 @@
typeModel
| (reexport).func | reexport | Member[func] |
| upstream-lib | (reexport).func | ReturnValue |
| upstream-lib | reexport | Member[lib] |
| upstream-lib.XYZ | reexport | Member[x].Member[y].Member[z] |
| upstream-lib.XYZ | reexport | Member[xy].Member[z] |
summaryModel

View File

@@ -8,6 +8,8 @@ module ModelExportConfig implements ModelExportSig {
}
predicate mustBeNamed(API::Node node) { shouldContain(node) }
predicate shouldContainType(string type) { Shared::isRelevantType(type) }
}
module Exported = ModelExport<ModelExportConfig>;