mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
Merge pull request #15386 from asgerf/js/graph-export
JS: Add library for exporting graphs as type models
This commit is contained in:
@@ -501,16 +501,25 @@ module API {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location of this API node, if it corresponds to a program element with a source location.
|
||||
*/
|
||||
final Location getLocation() { result = this.getInducingNode().getLocation() }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `getLocation().hasLocationInfo()` instead.
|
||||
*
|
||||
* Holds if this node is located in file `path` between line `startline`, column `startcol`,
|
||||
* and line `endline`, column `endcol`.
|
||||
*
|
||||
* For nodes that do not have a meaningful location, `path` is the empty string and all other
|
||||
* parameters are zero.
|
||||
*/
|
||||
predicate hasLocationInfo(string path, int startline, int startcol, int endline, int endcol) {
|
||||
this.getInducingNode().hasLocationInfo(path, startline, startcol, endline, endcol)
|
||||
deprecated predicate hasLocationInfo(
|
||||
string path, int startline, int startcol, int endline, int endcol
|
||||
) {
|
||||
this.getLocation().hasLocationInfo(path, startline, startcol, endline, endcol)
|
||||
or
|
||||
not exists(this.getInducingNode()) and
|
||||
not exists(this.getLocation()) and
|
||||
path = "" and
|
||||
startline = 0 and
|
||||
startcol = 0 and
|
||||
@@ -696,14 +705,7 @@ module API {
|
||||
or
|
||||
any(Type t).hasUnderlyingType(m, _)
|
||||
} or
|
||||
MkClassInstance(DataFlow::ClassNode cls) {
|
||||
hasSemantics(cls) and
|
||||
(
|
||||
cls = trackDefNode(_)
|
||||
or
|
||||
cls.getAnInstanceReference() = trackDefNode(_)
|
||||
)
|
||||
} or
|
||||
MkClassInstance(DataFlow::ClassNode cls) { needsDefNode(cls) } or
|
||||
MkDef(DataFlow::Node nd) { rhs(_, _, nd) } or
|
||||
MkUse(DataFlow::Node nd) { use(_, _, nd) } or
|
||||
/** A use of a TypeScript type. */
|
||||
@@ -716,6 +718,17 @@ module API {
|
||||
trackUseNode(src, true, bound, "").flowsTo(nd.getCalleeNode())
|
||||
}
|
||||
|
||||
private predicate needsDefNode(DataFlow::ClassNode cls) {
|
||||
hasSemantics(cls) and
|
||||
(
|
||||
cls = trackDefNode(_)
|
||||
or
|
||||
cls.getAnInstanceReference() = trackDefNode(_)
|
||||
or
|
||||
needsDefNode(cls.getADirectSubClass())
|
||||
)
|
||||
}
|
||||
|
||||
class TDef = MkModuleDef or TNonModuleDef;
|
||||
|
||||
class TNonModuleDef = MkModuleExport or MkClassInstance or MkDef or MkSyntheticCallbackArg;
|
||||
@@ -1306,7 +1319,7 @@ module API {
|
||||
succ = MkDef(rhs)
|
||||
or
|
||||
exists(DataFlow::ClassNode cls |
|
||||
cls.getAnInstanceReference() = rhs and
|
||||
cls.getAnInstanceReference().flowsTo(rhs) and
|
||||
succ = MkClassInstance(cls)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,7 +147,11 @@ private predicate isPrivateAssignment(DataFlow::Node node) {
|
||||
)
|
||||
}
|
||||
|
||||
private predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
|
||||
/**
|
||||
* Holds if `node` is the sink node corresponding to the right-hand side of a private declaration,
|
||||
* like a private field (`#field`) or class member with the `private` modifier.
|
||||
*/
|
||||
predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
|
||||
|
||||
bindingset[name]
|
||||
private int getNameBadness(string name) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
private import javascript
|
||||
private import internal.ApiGraphModels as Shared
|
||||
private import internal.ApiGraphModelsSpecific as Specific
|
||||
private import semmle.javascript.endpoints.EndpointNaming as EndpointNaming
|
||||
import Shared::ModelInput as ModelInput
|
||||
import Shared::ModelOutput as ModelOutput
|
||||
|
||||
@@ -55,3 +56,106 @@ private class TaintStepFromSummary extends TaintTracking::SharedTaintStep {
|
||||
summaryStepNodes(pred, succ, "taint")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which parts of the API graph to export in `ModelExport`.
|
||||
*/
|
||||
signature module ModelExportSig {
|
||||
/**
|
||||
* Holds if the exported model should contain `node`, if it is publicly accessible.
|
||||
*
|
||||
* This ensures that all ways to access `node` will be exported in type models.
|
||||
*/
|
||||
predicate shouldContain(API::Node node);
|
||||
|
||||
/**
|
||||
* Holds if `node` must be named if it is part of 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() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Module for exporting type models for a given set of nodes in the API graph.
|
||||
*/
|
||||
module ModelExport<ModelExportSig S> {
|
||||
private import codeql.mad.dynamic.GraphExport
|
||||
private import internal.ApiGraphModelsExport
|
||||
|
||||
private module GraphExportConfig implements GraphExportSig<Location, API::Node> {
|
||||
predicate edge = Specific::apiGraphHasEdge/3;
|
||||
|
||||
predicate shouldContain = S::shouldContain/1;
|
||||
|
||||
predicate shouldNotContain(API::Node node) {
|
||||
EndpointNaming::isPrivateLike(node)
|
||||
or
|
||||
node instanceof API::Use
|
||||
}
|
||||
|
||||
predicate mustBeNamed(API::Node node) {
|
||||
node.getAValueReachingSink() instanceof DataFlow::ClassNode
|
||||
or
|
||||
node = API::Internal::getClassInstance(_)
|
||||
or
|
||||
S::mustBeNamed(node)
|
||||
}
|
||||
|
||||
predicate exposedName(API::Node node, string type, string path) {
|
||||
node = API::moduleExport(type) and path = ""
|
||||
}
|
||||
|
||||
predicate suggestedName(API::Node node, string type) {
|
||||
exists(string package, string name |
|
||||
(
|
||||
EndpointNaming::sinkHasPrimaryName(node, package, name) and
|
||||
not EndpointNaming::aliasDefinition(_, _, _, _, node)
|
||||
or
|
||||
EndpointNaming::aliasDefinition(_, _, package, name, node)
|
||||
) and
|
||||
type = EndpointNaming::renderName(package, name)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[host]
|
||||
predicate hasTypeSummary(API::Node host, string path) {
|
||||
exists(string methodName |
|
||||
functionReturnsReceiver(host.getMember(methodName).getAValueReachingSink()) and
|
||||
path = "Member[" + methodName + "].ReturnValue"
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate functionReturnsReceiver(DataFlow::FunctionNode func) {
|
||||
getAReceiverRef(func).flowsTo(func.getReturnNode())
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private DataFlow::MethodCallNode getAReceiverCall(DataFlow::FunctionNode func) {
|
||||
result = getAReceiverRef(func).getAMethodCall()
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate callReturnsReceiver(DataFlow::MethodCallNode call) {
|
||||
functionReturnsReceiver(call.getACallee().flow())
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private DataFlow::SourceNode getAReceiverRef(DataFlow::FunctionNode func) {
|
||||
result = func.getReceiver()
|
||||
or
|
||||
result = getAReceiverCall(func) and
|
||||
callReturnsReceiver(result)
|
||||
}
|
||||
}
|
||||
|
||||
private module ExportedGraph = TypeGraphExport<GraphExportConfig, S::shouldContainType/1>;
|
||||
|
||||
import ExportedGraph
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ private predicate summaryModel(
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
any(DeprecationAdapter a).typeModel(type1, type2, path)
|
||||
or
|
||||
Extensions::typeModel(type1, type2, path)
|
||||
@@ -500,7 +500,7 @@ private API::Node getNodeFromType(string type) {
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(type, path) and
|
||||
(
|
||||
n = 0 and
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* See concrete examples in `TypeGraphExport`.
|
||||
*/
|
||||
bindingset[type, path]
|
||||
private predicate partiallyEvaluatedModel(
|
||||
string type, AccessPath path, API::Node node, string remainingPath
|
||||
) {
|
||||
exists(int n |
|
||||
getNodeFromPath(type, path, n) = node and
|
||||
n > 0 and
|
||||
// Note that `n < path.getNumToken()` is implied by the use of strictconcat()
|
||||
remainingPath =
|
||||
strictconcat(int k | k = [n .. path.getNumToken() - 1] | path.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<Specific::Location, 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<Specific::Location, 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<Specific::Location, 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
|
||||
Specific::sourceFlowsToSink(ModelOutput::getATypeNode(type1), node) 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
|
||||
Specific::sourceFlowsToSink(source, node) and
|
||||
ExportedGraph::pathToNode(type2, prefix, node) and
|
||||
path = join(prefix, remainingPath)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ module API = JS::API;
|
||||
|
||||
import JS::DataFlow as DataFlow
|
||||
|
||||
class Location = JS::Location;
|
||||
|
||||
/**
|
||||
* Holds if `rawType` represents the JavaScript type `qualifiedName` from the given NPM `package`.
|
||||
*
|
||||
@@ -353,3 +355,54 @@ module ModelOutputSpecific {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the edge `pred -> succ` labelled with `path` exists in the API graph.
|
||||
*/
|
||||
bindingset[pred]
|
||||
predicate apiGraphHasEdge(API::Node pred, string path, API::Node succ) {
|
||||
exists(string name | succ = pred.getMember(name) and path = "Member[" + name + "]")
|
||||
or
|
||||
succ = pred.getUnknownMember() and path = "AnyMember"
|
||||
or
|
||||
succ = pred.getInstance() and path = "Instance"
|
||||
or
|
||||
succ = pred.getReturn() and path = "ReturnValue"
|
||||
or
|
||||
exists(int n | succ = pred.getParameter(n) |
|
||||
if pred instanceof API::Use then path = "Argument[" + n + "]" else path = "Parameter[" + n + "]"
|
||||
)
|
||||
or
|
||||
succ = pred.getPromised() and path = "Awaited"
|
||||
or
|
||||
exists(DataFlow::ClassNode cls |
|
||||
pred = API::Internal::getClassInstance(cls.getADirectSubClass()) and
|
||||
succ = API::Internal::getClassInstance(cls) and
|
||||
path = ""
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the value of `source` is exposed at `sink`.
|
||||
*/
|
||||
bindingset[source]
|
||||
predicate sourceFlowsToSink(API::Node source, API::Node sink) {
|
||||
source.getAValueReachableFromSource() = sink.asSink()
|
||||
or
|
||||
// Handle the case of an upstream class being the base class of an exposed own class
|
||||
//
|
||||
// class Foo extends external.BaseClass {}
|
||||
//
|
||||
// Here we want to ensure that `Instance(Foo)` is seen as subtype of `Instance(external.BaseClass)`.
|
||||
//
|
||||
// Although we have a dedicated sink node for `Instance(Foo)` we don't have dedicate source node for `Instance(external.BaseClass)`.
|
||||
//
|
||||
// However, there is always an `Instance` edge from the base class expression (`external.BaseClass`)
|
||||
// to the receiver node in subclass constructor (the implicit constructor of `Foo`), which always exists.
|
||||
// So we use the constructor receiver as the representative for `Instance(external.BaseClass)`.
|
||||
// (This will get simplified when migrating to Ruby-style API graphs, as both sides will have explicit API nodes).
|
||||
exists(DataFlow::ClassNode cls |
|
||||
source.asSource() = cls.getConstructor().getReceiver() and
|
||||
sink = API::Internal::getClassInstance(cls)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
typeModel
|
||||
| (aliases).Alias1 | aliases | Member[Alias1] |
|
||||
| (aliases).Alias1 | aliases | Member[Alias2] |
|
||||
| (aliases).Alias1 | aliases | Member[Alias3].Member[x] |
|
||||
| (aliases).Alias1 | aliases | Member[Alias4].Member[x].Member[x] |
|
||||
| (aliases).Alias1 | aliases | Member[AliasedClass] |
|
||||
| (aliases).Alias1.prototype | (aliases).Alias1 | Instance |
|
||||
| (aliases).Alias1.prototype | (aliases).Alias1.prototype.foo | ReturnValue |
|
||||
| (aliases).Alias1.prototype.foo | (aliases).Alias1.prototype | Member[foo] |
|
||||
| (long-access-path).a.shortcut.d | long-access-path | Member[a].Member[b].Member[c].Member[d] |
|
||||
| (long-access-path).a.shortcut.d | long-access-path | Member[a].Member[shortcut].Member[d] |
|
||||
| (long-access-path).a.shortcut.d.e | (long-access-path).a.shortcut.d | Member[e] |
|
||||
| (reexport).func | reexport | Member[func] |
|
||||
| (return-this).FluentInterface | return-this | Member[FluentInterface] |
|
||||
| (return-this).FluentInterface.prototype | (return-this).FluentInterface | Instance |
|
||||
| (return-this).FluentInterface.prototype | (return-this).FluentInterface.prototype.bar | ReturnValue |
|
||||
| (return-this).FluentInterface.prototype | (return-this).FluentInterface.prototype.baz | ReturnValue |
|
||||
| (return-this).FluentInterface.prototype | (return-this).FluentInterface.prototype.foo | ReturnValue |
|
||||
| (return-this).FluentInterface.prototype.bar | (return-this).FluentInterface.prototype | Member[bar] |
|
||||
| (return-this).FluentInterface.prototype.baz | (return-this).FluentInterface.prototype | Member[baz] |
|
||||
| (return-this).FluentInterface.prototype.foo | (return-this).FluentInterface.prototype | Member[foo] |
|
||||
| (return-this).FluentInterface.prototype.notFluent | (return-this).FluentInterface.prototype | Member[notFluent] |
|
||||
| (return-this).FluentInterface.prototype.notFluent2 | (return-this).FluentInterface.prototype | Member[notFluent2] |
|
||||
| (root-function).PublicClass | root-function | Member[PublicClass] |
|
||||
| (root-function).PublicClass.prototype | (root-function).PublicClass | Instance |
|
||||
| (root-function).PublicClass.prototype | root-function | ReturnValue |
|
||||
| (root-function).PublicClass.prototype.method | (root-function).PublicClass.prototype | Member[method] |
|
||||
| (semi-internal-class).PublicClass | semi-internal-class | Member[PublicClass] |
|
||||
| (semi-internal-class).PublicClass.prototype | (semi-internal-class).PublicClass | Instance |
|
||||
| (semi-internal-class).PublicClass.prototype | (semi-internal-class).SemiInternalClass.prototype.method | ReturnValue |
|
||||
| (semi-internal-class).PublicClass.prototype | (semi-internal-class).getAnonymous~expr2 | ReturnValue |
|
||||
| (semi-internal-class).PublicClass.prototype.publicMethod | (semi-internal-class).PublicClass.prototype | Member[publicMethod] |
|
||||
| (semi-internal-class).SemiInternalClass.prototype | (semi-internal-class).get | ReturnValue |
|
||||
| (semi-internal-class).SemiInternalClass.prototype.method | (semi-internal-class).SemiInternalClass.prototype | Member[method] |
|
||||
| (semi-internal-class).get | semi-internal-class | Member[get] |
|
||||
| (semi-internal-class).getAnonymous | semi-internal-class | Member[getAnonymous] |
|
||||
| (semi-internal-class).getAnonymous~expr1 | (semi-internal-class).getAnonymous | ReturnValue |
|
||||
| (semi-internal-class).getAnonymous~expr2 | (semi-internal-class).getAnonymous~expr1 | Member[method] |
|
||||
| (subclass).A | subclass | Member[A] |
|
||||
| (subclass).A.prototype | (subclass).A | Instance |
|
||||
| (subclass).A.prototype | (subclass).B.prototype | |
|
||||
| (subclass).A.prototype | (subclass).ExposedMidSubClass.prototype~expr1 | |
|
||||
| (subclass).A.prototype.a | (subclass).A.prototype | Member[a] |
|
||||
| (subclass).B | subclass | Member[B] |
|
||||
| (subclass).B.prototype | (subclass).B | Instance |
|
||||
| (subclass).B.prototype | (subclass).C.prototype | |
|
||||
| (subclass).B.prototype.b | (subclass).B.prototype | Member[b] |
|
||||
| (subclass).C | subclass | Member[C] |
|
||||
| (subclass).C.prototype | (subclass).C | Instance |
|
||||
| (subclass).C.prototype.c | (subclass).C.prototype | Member[c] |
|
||||
| (subclass).D | subclass | Member[D] |
|
||||
| (subclass).D.prototype | (subclass).D | Instance |
|
||||
| (subclass).D.prototype.d | (subclass).D.prototype | Member[d] |
|
||||
| (subclass).ExposedMidSubClass | subclass | Member[ExposedMidSubClass] |
|
||||
| (subclass).ExposedMidSubClass.prototype | (subclass).ExposedMidSubClass | Instance |
|
||||
| (subclass).ExposedMidSubClass.prototype.m | (subclass).ExposedMidSubClass.prototype | Member[m] |
|
||||
| (subclass).ExposedMidSubClass.prototype~expr1 | (subclass).ExposedMidSubClass.prototype | |
|
||||
| upstream-lib | (reexport).func | ReturnValue |
|
||||
| upstream-lib | reexport | Member[lib] |
|
||||
| upstream-lib.Type | (subclass).D.prototype | |
|
||||
| upstream-lib.XYZ | reexport | Member[x].Member[y].Member[z] |
|
||||
| upstream-lib.XYZ | reexport | Member[xy].Member[z] |
|
||||
summaryModel
|
||||
| (aliases).Alias1.prototype | | | Member[foo].ReturnValue | type |
|
||||
| (return-this).FluentInterface.prototype | | | Member[bar].ReturnValue | type |
|
||||
| (return-this).FluentInterface.prototype | | | Member[baz].ReturnValue | type |
|
||||
| (return-this).FluentInterface.prototype | | | Member[foo].ReturnValue | type |
|
||||
@@ -0,0 +1,7 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/javascript-all
|
||||
extensible: typeModel
|
||||
data:
|
||||
- ["upstream-lib.XYZ", "upstream-lib", "Member[x].Member[y].Member[z]"]
|
||||
- ["upstream-lib.Type", "upstream-lib", "Member[Type].Instance"]
|
||||
@@ -0,0 +1,19 @@
|
||||
private import javascript
|
||||
private import semmle.javascript.endpoints.EndpointNaming as EndpointNaming
|
||||
private import semmle.javascript.frameworks.data.internal.ApiGraphModels as Shared
|
||||
|
||||
module ModelExportConfig implements ModelExportSig {
|
||||
predicate shouldContain(API::Node node) {
|
||||
node.getAValueReachingSink() instanceof DataFlow::FunctionNode
|
||||
}
|
||||
|
||||
predicate mustBeNamed(API::Node node) { shouldContain(node) }
|
||||
|
||||
predicate shouldContainType(string type) { Shared::isRelevantType(type) }
|
||||
}
|
||||
|
||||
module Exported = ModelExport<ModelExportConfig>;
|
||||
|
||||
query predicate typeModel = Exported::typeModel/3;
|
||||
|
||||
query predicate summaryModel = Exported::summaryModel/5;
|
||||
@@ -0,0 +1,9 @@
|
||||
export class AliasedClass {
|
||||
foo() { return this; }
|
||||
}
|
||||
|
||||
export const Alias1 = AliasedClass;
|
||||
export const Alias2 = AliasedClass;
|
||||
|
||||
export const Alias3 = { x: AliasedClass };
|
||||
export const Alias4 = { x: Alias3 };
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "aliases",
|
||||
"main": "aliases.js"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const a = {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: function() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
a.shortcut = a.b.c;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "long-access-path",
|
||||
"main": "long-access-path.js"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "reexport",
|
||||
"main": "reexport.js"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as lib from "upstream-lib";
|
||||
|
||||
export { lib };
|
||||
|
||||
export const x = lib.x;
|
||||
export const xy = lib.x.y;
|
||||
|
||||
export function func() {
|
||||
return lib;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "return-this",
|
||||
"main": "return-this.js"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export class FluentInterface {
|
||||
foo() {
|
||||
return this;
|
||||
}
|
||||
bar() {
|
||||
return this.foo();
|
||||
}
|
||||
baz() {
|
||||
return this.foo().bar().bar().foo();
|
||||
}
|
||||
notFluent() {
|
||||
this.foo();
|
||||
}
|
||||
notFluent2() {
|
||||
return this.notFluent2();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "root-function",
|
||||
"main": "root-function.js"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class C {
|
||||
method() {}
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
return new C();
|
||||
}
|
||||
|
||||
module.exports.PublicClass = C;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "semi-internal-class",
|
||||
"main": "semi-internal-class.js"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export class PublicClass {
|
||||
publicMethod() {}
|
||||
}
|
||||
|
||||
class SemiInternalClass {
|
||||
method() {
|
||||
return new PublicClass();
|
||||
}
|
||||
}
|
||||
|
||||
export function get() {
|
||||
return new SemiInternalClass();
|
||||
}
|
||||
|
||||
export function getAnonymous() {
|
||||
return new (class {
|
||||
method() {
|
||||
return new PublicClass();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "subclass",
|
||||
"main": "subclass.js"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export class A {
|
||||
a() {}
|
||||
}
|
||||
|
||||
export class B extends A {
|
||||
b() {}
|
||||
}
|
||||
|
||||
export class C extends B {
|
||||
c() {}
|
||||
}
|
||||
|
||||
import * as upstream from "upstream-lib";
|
||||
|
||||
export class D extends upstream.Type {
|
||||
d() {}
|
||||
}
|
||||
|
||||
// Test case where subclass chain goes through an internal class
|
||||
class InternalMidClass extends A {}
|
||||
|
||||
export class ExposedMidSubClass extends InternalMidClass {
|
||||
m() {}
|
||||
}
|
||||
@@ -341,7 +341,7 @@ private predicate summaryModel(
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
any(DeprecationAdapter a).typeModel(type1, type2, path)
|
||||
or
|
||||
Extensions::typeModel(type1, type2, path)
|
||||
@@ -500,7 +500,7 @@ private API::Node getNodeFromType(string type) {
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(type, path) and
|
||||
(
|
||||
n = 0 and
|
||||
|
||||
@@ -341,7 +341,7 @@ private predicate summaryModel(
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
any(DeprecationAdapter a).typeModel(type1, type2, path)
|
||||
or
|
||||
Extensions::typeModel(type1, type2, path)
|
||||
@@ -500,7 +500,7 @@ private API::Node getNodeFromType(string type) {
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
API::Node getNodeFromPath(string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(type, path) and
|
||||
(
|
||||
n = 0 and
|
||||
|
||||
238
shared/mad/codeql/mad/dynamic/GraphExport.qll
Normal file
238
shared/mad/codeql/mad/dynamic/GraphExport.qll
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Contains predicates for converting an arbitrary graph to a set of `typeModel` rows.
|
||||
*/
|
||||
|
||||
private import codeql.util.Location
|
||||
|
||||
/**
|
||||
* Concatenates two access paths, separating them by `.` unless one of them is empty.
|
||||
*/
|
||||
bindingset[x, y]
|
||||
string join(string x, string y) {
|
||||
if x = "" or y = "" then result = x + y else result = x + "." + y
|
||||
}
|
||||
|
||||
private module WithLocation<LocationSig Location> {
|
||||
signature class NodeSig {
|
||||
/** Gets the location of this node if it has one. */
|
||||
Location getLocation();
|
||||
|
||||
/** Gets a string representation of this node. */
|
||||
string toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a graph to export in `GraphExport`.
|
||||
*/
|
||||
signature module GraphExportSig<LocationSig Location, WithLocation<Location>::NodeSig Node> {
|
||||
/**
|
||||
* Holds if an edge `pred -> succ` exist with the access path `path`.
|
||||
*/
|
||||
bindingset[pred]
|
||||
predicate edge(Node pred, string path, Node succ);
|
||||
|
||||
/**
|
||||
* Holds if `node` is exposed to downstream packages with the given `(type, path)` tuple.
|
||||
*
|
||||
* A consumer of the exported graph should be able to interpret the `(type, path)` pair
|
||||
* without having access to the current codebase.
|
||||
*/
|
||||
predicate exposedName(Node node, string type, string path);
|
||||
|
||||
/**
|
||||
* Holds if `name` is a good name for `node` that should be used in case the node needs
|
||||
* to be named with a type name.
|
||||
*
|
||||
* Should not hold for nodes that are named via `exposedName`.
|
||||
*/
|
||||
predicate suggestedName(Node node, string name);
|
||||
|
||||
/**
|
||||
* Holds if `node` must be named if it is part of the exported graph.
|
||||
*/
|
||||
predicate mustBeNamed(Node node);
|
||||
|
||||
/**
|
||||
* Holds if the exported graph should contain `node`, if it is reachable from an exposed node.
|
||||
*
|
||||
* This ensures that all paths leading from an exposed node to `node` will be exported.
|
||||
*/
|
||||
predicate shouldContain(Node node);
|
||||
|
||||
/**
|
||||
* Holds if `host` has a method that returns `this`, and `path` is the path from `host`
|
||||
* to the method followed by the appropriate `ReturnValue` token.
|
||||
*
|
||||
* For example, if the method is named `m` then `path` should be `Member[m].ReturnValue`
|
||||
* or `Method[m].ReturnValue`.
|
||||
*/
|
||||
bindingset[host]
|
||||
predicate hasTypeSummary(Node host, string path);
|
||||
|
||||
/**
|
||||
* Holds if paths going through `node` should be blocked.
|
||||
*
|
||||
* For example, this can be the case for functions that are public at runtime
|
||||
* but intended to be private.
|
||||
*/
|
||||
predicate shouldNotContain(Node node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Module for exporting an arbitrary graph as models-as-data rows.
|
||||
*/
|
||||
module GraphExport<
|
||||
LocationSig Location, WithLocation<Location>::NodeSig Node, GraphExportSig<Location, Node> S>
|
||||
{
|
||||
private import S
|
||||
|
||||
private Node getAnExposedNode() {
|
||||
not shouldNotContain(result) and
|
||||
(
|
||||
exposedName(result, _, _)
|
||||
or
|
||||
edge(getAnExposedNode(), _, result)
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate exposedEdge(Node pred, string path, Node succ) {
|
||||
// Materialize this relation so we can access 'edge' without binding set on 'pred'
|
||||
pred = getAnExposedNode() and
|
||||
edge(pred, path, succ)
|
||||
}
|
||||
|
||||
private Node getARelevantNode() {
|
||||
result = getAnExposedNode() and
|
||||
shouldContain(result)
|
||||
or
|
||||
exposedEdge(result, _, getARelevantNode())
|
||||
}
|
||||
|
||||
final private class FinalNode = Node;
|
||||
|
||||
private class RelevantNode extends FinalNode {
|
||||
RelevantNode() { this = getARelevantNode() }
|
||||
}
|
||||
|
||||
pragma[inline]
|
||||
private RelevantNode getAPredecessor(RelevantNode node, string path) {
|
||||
exposedEdge(result, path, node)
|
||||
}
|
||||
|
||||
private predicate hasPrettyName(RelevantNode node) {
|
||||
exposedName(node, _, "")
|
||||
or
|
||||
suggestedName(node, _)
|
||||
}
|
||||
|
||||
private predicate nodeMustBeNamed(RelevantNode node) {
|
||||
exposedName(node, _, "")
|
||||
or
|
||||
S::mustBeNamed(node)
|
||||
or
|
||||
strictcount(getAPredecessor(node, _)) > 1
|
||||
}
|
||||
|
||||
/** Gets a type-name to use as a prefix, in case we need to synthesize a name. */
|
||||
private string getAPrefixTypeName(RelevantNode node) {
|
||||
result =
|
||||
min(string prefix |
|
||||
exists(string type, string path |
|
||||
exposedName(node, type, path) and
|
||||
prefix = join(type, path)
|
||||
)
|
||||
or
|
||||
suggestedName(node, prefix)
|
||||
|
|
||||
prefix
|
||||
)
|
||||
or
|
||||
not hasPrettyName(node) and
|
||||
result = getAPrefixTypeName(getAPredecessor(node, _))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a synthetic name must be generated for `node`.
|
||||
*/
|
||||
private predicate isSyntheticallyNamedNode(RelevantNode node, string prefix) {
|
||||
nodeMustBeNamed(node) and
|
||||
not hasPrettyName(node) and
|
||||
prefix = min(getAPrefixTypeName(node))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a synthetic type name to generate for `node`, if it doesn't have a pretty name.
|
||||
*/
|
||||
private string getSyntheticName(RelevantNode node) {
|
||||
exists(int k, string prefixTypeName |
|
||||
node =
|
||||
rank[k](RelevantNode n, string path, int startline, int startcol, int endline, int endcol |
|
||||
isSyntheticallyNamedNode(n, prefixTypeName) and
|
||||
n.getLocation().hasLocationInfo(path, startline, startcol, endline, endcol)
|
||||
|
|
||||
// Use location information for an arbitrary ordering
|
||||
// TODO: improve support for nodes without a location, currently they can cause FNs
|
||||
n order by path, startline, startcol, endline, endcol
|
||||
) and
|
||||
result = prefixTypeName + "~expr" + k
|
||||
)
|
||||
}
|
||||
|
||||
private string getNodeName(RelevantNode node) {
|
||||
nodeMustBeNamed(node) and
|
||||
(
|
||||
exposedName(node, result, "")
|
||||
or
|
||||
suggestedName(node, result) and
|
||||
not exposedName(node, _, "")
|
||||
or
|
||||
result = getSyntheticName(node)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(type, path)` resolves to `node` in the exported graph.
|
||||
*/
|
||||
predicate pathToNode(string type, string path, RelevantNode node) {
|
||||
type = getNodeName(node) and
|
||||
path = ""
|
||||
or
|
||||
exposedName(node, type, path)
|
||||
or
|
||||
not nodeMustBeNamed(node) and
|
||||
exists(string prevPath, string step |
|
||||
pathToNode(type, prevPath, getAPredecessor(node, step)) and
|
||||
path = join(prevPath, step)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `type1, type2, path` should be emitted as a type row.
|
||||
*
|
||||
* That is, `(type2, path)` leads to an value belonging to `type1`.
|
||||
*/
|
||||
predicate typeModel(string type1, string type2, string path) {
|
||||
exists(string prevPath, string step, RelevantNode node |
|
||||
type1 = getNodeName(node) and
|
||||
pathToNode(type2, prevPath, getAPredecessor(node, step)) and
|
||||
path = join(prevPath, step)
|
||||
) and
|
||||
not (type1 = type2 and path = "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `type, path, input, output, kind` should be emitted as a summary row.
|
||||
*
|
||||
* This is only used to emit type propagation summaries, that is, summaries of kind `type`.
|
||||
*/
|
||||
predicate summaryModel(string type, string path, string input, string output, string kind) {
|
||||
exists(RelevantNode host |
|
||||
pathToNode(type, path, host) and
|
||||
hasTypeSummary(host, output) and
|
||||
input = "" and
|
||||
kind = "type"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,6 @@ name: codeql/mad
|
||||
version: 0.2.15-dev
|
||||
groups: shared
|
||||
library: true
|
||||
dependencies: null
|
||||
dependencies:
|
||||
codeql/util: ${workspace}
|
||||
warnOnImplicitThis: true
|
||||
|
||||
Reference in New Issue
Block a user