Skip to content

Commit decd576

Browse files
authored
Merge pull request #15386 from asgerf/js/graph-export
JS: Add library for exporting graphs as type models
2 parents 622f69e + 3c885f3 commit decd576

File tree

27 files changed

+778
-20
lines changed

27 files changed

+778
-20
lines changed

javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -501,16 +501,25 @@ module API {
501501
}
502502

503503
/**
504+
* Gets the location of this API node, if it corresponds to a program element with a source location.
505+
*/
506+
final Location getLocation() { result = this.getInducingNode().getLocation() }
507+
508+
/**
509+
* DEPRECATED: Use `getLocation().hasLocationInfo()` instead.
510+
*
504511
* Holds if this node is located in file `path` between line `startline`, column `startcol`,
505512
* and line `endline`, column `endcol`.
506513
*
507514
* For nodes that do not have a meaningful location, `path` is the empty string and all other
508515
* parameters are zero.
509516
*/
510-
predicate hasLocationInfo(string path, int startline, int startcol, int endline, int endcol) {
511-
this.getInducingNode().hasLocationInfo(path, startline, startcol, endline, endcol)
517+
deprecated predicate hasLocationInfo(
518+
string path, int startline, int startcol, int endline, int endcol
519+
) {
520+
this.getLocation().hasLocationInfo(path, startline, startcol, endline, endcol)
512521
or
513-
not exists(this.getInducingNode()) and
522+
not exists(this.getLocation()) and
514523
path = "" and
515524
startline = 0 and
516525
startcol = 0 and
@@ -696,14 +705,7 @@ module API {
696705
or
697706
any(Type t).hasUnderlyingType(m, _)
698707
} or
699-
MkClassInstance(DataFlow::ClassNode cls) {
700-
hasSemantics(cls) and
701-
(
702-
cls = trackDefNode(_)
703-
or
704-
cls.getAnInstanceReference() = trackDefNode(_)
705-
)
706-
} or
708+
MkClassInstance(DataFlow::ClassNode cls) { needsDefNode(cls) } or
707709
MkDef(DataFlow::Node nd) { rhs(_, _, nd) } or
708710
MkUse(DataFlow::Node nd) { use(_, _, nd) } or
709711
/** A use of a TypeScript type. */
@@ -716,6 +718,17 @@ module API {
716718
trackUseNode(src, true, bound, "").flowsTo(nd.getCalleeNode())
717719
}
718720

721+
private predicate needsDefNode(DataFlow::ClassNode cls) {
722+
hasSemantics(cls) and
723+
(
724+
cls = trackDefNode(_)
725+
or
726+
cls.getAnInstanceReference() = trackDefNode(_)
727+
or
728+
needsDefNode(cls.getADirectSubClass())
729+
)
730+
}
731+
719732
class TDef = MkModuleDef or TNonModuleDef;
720733

721734
class TNonModuleDef = MkModuleExport or MkClassInstance or MkDef or MkSyntheticCallbackArg;
@@ -1306,7 +1319,7 @@ module API {
13061319
succ = MkDef(rhs)
13071320
or
13081321
exists(DataFlow::ClassNode cls |
1309-
cls.getAnInstanceReference() = rhs and
1322+
cls.getAnInstanceReference().flowsTo(rhs) and
13101323
succ = MkClassInstance(cls)
13111324
)
13121325
)

javascript/ql/lib/semmle/javascript/endpoints/EndpointNaming.qll

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ private predicate isPrivateAssignment(DataFlow::Node node) {
147147
)
148148
}
149149

150-
private predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
150+
/**
151+
* Holds if `node` is the sink node corresponding to the right-hand side of a private declaration,
152+
* like a private field (`#field`) or class member with the `private` modifier.
153+
*/
154+
predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
151155

152156
bindingset[name]
153157
private int getNameBadness(string name) {

javascript/ql/lib/semmle/javascript/frameworks/data/ModelsAsData.qll

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
private import javascript
2020
private import internal.ApiGraphModels as Shared
2121
private import internal.ApiGraphModelsSpecific as Specific
22+
private import semmle.javascript.endpoints.EndpointNaming as EndpointNaming
2223
import Shared::ModelInput as ModelInput
2324
import Shared::ModelOutput as ModelOutput
2425

@@ -55,3 +56,106 @@ private class TaintStepFromSummary extends TaintTracking::SharedTaintStep {
5556
summaryStepNodes(pred, succ, "taint")
5657
}
5758
}
59+
60+
/**
61+
* Specifies which parts of the API graph to export in `ModelExport`.
62+
*/
63+
signature module ModelExportSig {
64+
/**
65+
* Holds if the exported model should contain `node`, if it is publicly accessible.
66+
*
67+
* This ensures that all ways to access `node` will be exported in type models.
68+
*/
69+
predicate shouldContain(API::Node node);
70+
71+
/**
72+
* Holds if `node` must be named if it is part of the exported graph.
73+
*/
74+
default predicate mustBeNamed(API::Node node) { none() }
75+
76+
/**
77+
* Holds if the exported model should preserve all paths leading to an instance of `type`,
78+
* including partial ones. It does not need to be closed transitively, `ModelExport` will
79+
* extend this to include type models from which `type` can be derived.
80+
*/
81+
default predicate shouldContainType(string type) { none() }
82+
}
83+
84+
/**
85+
* Module for exporting type models for a given set of nodes in the API graph.
86+
*/
87+
module ModelExport<ModelExportSig S> {
88+
private import codeql.mad.dynamic.GraphExport
89+
private import internal.ApiGraphModelsExport
90+
91+
private module GraphExportConfig implements GraphExportSig<Location, API::Node> {
92+
predicate edge = Specific::apiGraphHasEdge/3;
93+
94+
predicate shouldContain = S::shouldContain/1;
95+
96+
predicate shouldNotContain(API::Node node) {
97+
EndpointNaming::isPrivateLike(node)
98+
or
99+
node instanceof API::Use
100+
}
101+
102+
predicate mustBeNamed(API::Node node) {
103+
node.getAValueReachingSink() instanceof DataFlow::ClassNode
104+
or
105+
node = API::Internal::getClassInstance(_)
106+
or
107+
S::mustBeNamed(node)
108+
}
109+
110+
predicate exposedName(API::Node node, string type, string path) {
111+
node = API::moduleExport(type) and path = ""
112+
}
113+
114+
predicate suggestedName(API::Node node, string type) {
115+
exists(string package, string name |
116+
(
117+
EndpointNaming::sinkHasPrimaryName(node, package, name) and
118+
not EndpointNaming::aliasDefinition(_, _, _, _, node)
119+
or
120+
EndpointNaming::aliasDefinition(_, _, package, name, node)
121+
) and
122+
type = EndpointNaming::renderName(package, name)
123+
)
124+
}
125+
126+
bindingset[host]
127+
predicate hasTypeSummary(API::Node host, string path) {
128+
exists(string methodName |
129+
functionReturnsReceiver(host.getMember(methodName).getAValueReachingSink()) and
130+
path = "Member[" + methodName + "].ReturnValue"
131+
)
132+
}
133+
134+
pragma[nomagic]
135+
private predicate functionReturnsReceiver(DataFlow::FunctionNode func) {
136+
getAReceiverRef(func).flowsTo(func.getReturnNode())
137+
}
138+
139+
pragma[nomagic]
140+
private DataFlow::MethodCallNode getAReceiverCall(DataFlow::FunctionNode func) {
141+
result = getAReceiverRef(func).getAMethodCall()
142+
}
143+
144+
pragma[nomagic]
145+
private predicate callReturnsReceiver(DataFlow::MethodCallNode call) {
146+
functionReturnsReceiver(call.getACallee().flow())
147+
}
148+
149+
pragma[nomagic]
150+
private DataFlow::SourceNode getAReceiverRef(DataFlow::FunctionNode func) {
151+
result = func.getReceiver()
152+
or
153+
result = getAReceiverCall(func) and
154+
callReturnsReceiver(result)
155+
}
156+
}
157+
158+
private module ExportedGraph = TypeGraphExport<GraphExportConfig, S::shouldContainType/1>;
159+
160+
import ExportedGraph
161+
}

javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModels.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ private predicate summaryModel(
341341
}
342342

343343
/** Holds if a type model exists for the given parameters. */
344-
private predicate typeModel(string type1, string type2, string path) {
344+
predicate typeModel(string type1, string type2, string path) {
345345
any(DeprecationAdapter a).typeModel(type1, type2, path)
346346
or
347347
Extensions::typeModel(type1, type2, path)
@@ -500,7 +500,7 @@ private API::Node getNodeFromType(string type) {
500500
* Gets the API node identified by the first `n` tokens of `path` in the given `(type, path)` tuple.
501501
*/
502502
pragma[nomagic]
503-
private API::Node getNodeFromPath(string type, AccessPath path, int n) {
503+
API::Node getNodeFromPath(string type, AccessPath path, int n) {
504504
isRelevantFullPath(type, path) and
505505
(
506506
n = 0 and
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Contains an extension of `GraphExport` that relies on API graph specific functionality.
3+
*/
4+
5+
private import ApiGraphModels as Shared
6+
private import codeql.mad.dynamic.GraphExport
7+
private import ApiGraphModelsSpecific as Specific
8+
9+
private module API = Specific::API;
10+
11+
private import Shared
12+
13+
/**
14+
* Holds if some proper prefix of `(type, path)` evaluated to `node`, where `remainingPath`
15+
* is bound to the suffix of `path` that was not evaluated yet.
16+
*
17+
* See concrete examples in `TypeGraphExport`.
18+
*/
19+
bindingset[type, path]
20+
private predicate partiallyEvaluatedModel(
21+
string type, AccessPath path, API::Node node, string remainingPath
22+
) {
23+
exists(int n |
24+
getNodeFromPath(type, path, n) = node and
25+
n > 0 and
26+
// Note that `n < path.getNumToken()` is implied by the use of strictconcat()
27+
remainingPath =
28+
strictconcat(int k | k = [n .. path.getNumToken() - 1] | path.getToken(k), "." order by k)
29+
)
30+
}
31+
32+
/**
33+
* Holds if `type` and all types leading to `type` should be re-exported.
34+
*/
35+
signature predicate shouldContainTypeSig(string type);
36+
37+
/**
38+
* Wrapper around `GraphExport` that also exports information about re-exported types.
39+
*
40+
* ### JavaScript example 1
41+
* For example, suppose `shouldContainType("foo")` holds, and the following is the entry point for a package `bar`:
42+
* ```js
43+
* // bar.js
44+
* module.exports.xxx = require('foo');
45+
* ```
46+
* then this would generate the following type model:
47+
* ```
48+
* foo; bar; Member[xxx]
49+
* ```
50+
*
51+
* ### JavaScript example 2
52+
* For a more complex case, suppose the following type model exists:
53+
* ```
54+
* foo.XYZ; foo; Member[x].Member[y].Member[z]
55+
* ```
56+
* And the package exports something that matches a prefix of the access path above:
57+
* ```js
58+
* module.exports.blah = require('foo').x.y;
59+
* ```
60+
* This would result in the following type model:
61+
* ```
62+
* foo.XYZ; bar; Member[blah].Member[z]
63+
* ```
64+
* Notice that the access path `Member[blah].Member[z]` consists of an access path generated from the API
65+
* graph, with pieces of the access path from the original type model appended to it.
66+
*/
67+
module TypeGraphExport<
68+
GraphExportSig<Specific::Location, API::Node> S, shouldContainTypeSig/1 shouldContainType>
69+
{
70+
/** Like `shouldContainType` but includes types that lead to `type` via type models. */
71+
private predicate shouldContainTypeEx(string type) {
72+
shouldContainType(type)
73+
or
74+
exists(string prevType |
75+
shouldContainType(prevType) and
76+
Shared::typeModel(prevType, type, _)
77+
)
78+
}
79+
80+
private module Config implements GraphExportSig<Specific::Location, API::Node> {
81+
import S
82+
83+
predicate shouldContain(API::Node node) {
84+
S::shouldContain(node)
85+
or
86+
exists(string type1 | shouldContainTypeEx(type1) |
87+
ModelOutput::getATypeNode(type1).getAValueReachableFromSource() = node.asSink()
88+
or
89+
exists(string type2, string path |
90+
Shared::typeModel(type1, type2, path) and
91+
getNodeFromPath(type2, path, _).getAValueReachableFromSource() = node.asSink()
92+
)
93+
)
94+
}
95+
}
96+
97+
private module ExportedGraph = GraphExport<Specific::Location, API::Node, Config>;
98+
99+
import ExportedGraph
100+
101+
/**
102+
* Holds if `type1, type2, path` should be emitted as a type model, that is `(type2, path)` leads to an instance of `type1`.
103+
*/
104+
predicate typeModel(string type1, string type2, string path) {
105+
ExportedGraph::typeModel(type1, type2, path)
106+
or
107+
shouldContainTypeEx(type1) and
108+
exists(API::Node node |
109+
// A relevant type is exported directly
110+
Specific::sourceFlowsToSink(ModelOutput::getATypeNode(type1), node) and
111+
ExportedGraph::pathToNode(type2, path, node)
112+
or
113+
// Something that leads to a relevant type, but didn't finish its access path, is exported
114+
exists(string midType, string midPath, string remainingPath, string prefix, API::Node source |
115+
Shared::typeModel(type1, midType, midPath) and
116+
partiallyEvaluatedModel(midType, midPath, source, remainingPath) and
117+
Specific::sourceFlowsToSink(source, node) and
118+
ExportedGraph::pathToNode(type2, prefix, node) and
119+
path = join(prefix, remainingPath)
120+
)
121+
)
122+
}
123+
}

0 commit comments

Comments
 (0)