Skip to content

Commit 78125a7

Browse files
committed
Ruby: Model Editor support
Add experimental support for the CodeQL Model Editor.
1 parent 68a7734 commit 78125a7

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

ruby/ql/lib/codeql/ruby/security/SqlInjectionCustomizations.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import codeql.ruby.DataFlow
88
private import codeql.ruby.dataflow.BarrierGuards
99
private import codeql.ruby.dataflow.RemoteFlowSources
1010
private import codeql.ruby.ApiGraphs
11+
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
1112

1213
/**
1314
* Provides default sources, sinks and sanitizers for detecting SQL injection
@@ -56,4 +57,8 @@ module SqlInjection {
5657
{ }
5758

5859
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
60+
61+
private class ExternalSqlInjectionSink extends Sink {
62+
ExternalSqlInjectionSink() { this = ModelOutput::getASinkNode("sql-injection").asSink() }
63+
}
5964
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @name Fetch endpoints for use in the model editor (application mode)
3+
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
4+
* @kind table
5+
* @id rb/utils/modeleditor/application-mode-endpoints
6+
* @tags modeleditor endpoints application-mode
7+
*/
8+
9+
// This query is empty as Application Mode is not yet supported for Ruby.
10+
from int n
11+
where none()
12+
select n
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @name Fetch endpoints for use in the model editor (framework mode)
3+
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
4+
* @kind table
5+
* @id rb/utils/modeleditor/framework-mode-endpoints
6+
* @tags modeleditor endpoints framework-mode
7+
*/
8+
9+
import ruby
10+
import ModelEditor
11+
12+
from PublicEndpointFromSource endpoint
13+
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
14+
endpoint.getParameterTypes(), endpoint.getSupportedStatus(), endpoint.getFile().getBaseName(),
15+
endpoint.getSupportedType()
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/** Provides classes and predicates related to handling APIs for the VS Code extension. */
2+
3+
private import ruby
4+
private import codeql.ruby.dataflow.FlowSummary
5+
private import codeql.ruby.dataflow.internal.DataFlowPrivate
6+
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
7+
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
8+
private import codeql.ruby.frameworks.core.Gem
9+
private import codeql.ruby.frameworks.data.ModelsAsData
10+
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
11+
12+
/** Holds if the given callable is not worth supporting. */
13+
private predicate isUninteresting(DataFlow::MethodNode c) {
14+
c.getLocation().getFile() instanceof TestFile
15+
}
16+
17+
/**
18+
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
19+
*/
20+
class Endpoint extends DataFlow::MethodNode {
21+
Endpoint() { this.isPublic() and not isUninteresting(this) }
22+
23+
File getFile() { result = this.getLocation().getFile() }
24+
25+
string getName() { result = this.getMethodName() }
26+
27+
/**
28+
* Gets the namespace of this endpoint.
29+
*/
30+
bindingset[this]
31+
string getNamespace() {
32+
// Return the name of any gemspec file in the database.
33+
// TODO: make this work for projects with multiple gems (and hence multiple gemspec files)
34+
result = any(Gem::GemSpec g).getName()
35+
}
36+
37+
/**
38+
* Gets the unbound type name of this endpoint.
39+
*/
40+
bindingset[this]
41+
string getTypeName() {
42+
result =
43+
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
44+
.getQualifiedName()
45+
}
46+
47+
/**
48+
* Gets the parameter types of this endpoint.
49+
*/
50+
bindingset[this]
51+
string getParameterTypes() {
52+
// For now, return the names of postional parameters. We don't always have type information, so we can't return type names.
53+
// We don't yet handle keyword params, splat params or block params.
54+
result =
55+
"(" +
56+
concat(DataFlow::ParameterNode p, int i |
57+
p = this.asCallable().getParameter(i)
58+
|
59+
p.getName(), "," order by i
60+
) + ")"
61+
}
62+
63+
/** Holds if this API has a supported summary. */
64+
pragma[nomagic]
65+
predicate hasSummary() { none() }
66+
67+
/** Holds if this API is a known source. */
68+
pragma[nomagic]
69+
abstract predicate isSource();
70+
71+
/** Holds if this API is a known sink. */
72+
pragma[nomagic]
73+
abstract predicate isSink();
74+
75+
/** Holds if this API is a known neutral. */
76+
pragma[nomagic]
77+
predicate isNeutral() {
78+
none()
79+
// this instanceof FlowSummaryImpl::Public::NeutralCallable
80+
}
81+
82+
/**
83+
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
84+
* recognized source, sink or neutral or it has a flow summary.
85+
*/
86+
predicate isSupported() {
87+
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
88+
}
89+
90+
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
91+
92+
string getSupportedType() {
93+
this.isSink() and result = "sink"
94+
or
95+
this.isSource() and result = "source"
96+
or
97+
this.hasSummary() and result = "summary"
98+
or
99+
this.isNeutral() and result = "neutral"
100+
or
101+
not this.isSupported() and result = ""
102+
}
103+
}
104+
105+
string methodClassification(Call method) {
106+
method.getFile() instanceof TestFile and result = "test"
107+
or
108+
not method.getFile() instanceof TestFile and
109+
result = "source"
110+
}
111+
112+
class TestFile extends File {
113+
TestFile() { this.getRelativePath().regexpMatch(".*(test|spec).+") }
114+
}
115+
116+
/**
117+
* A callable where there exists a MaD sink model that applies to it.
118+
*/
119+
class SinkCallable extends DataFlow::MethodNode {
120+
SinkCallable() {
121+
this = ModelOutput::getASinkNode(_).asCallable() and
122+
exists(string type, string path, string kind, string method |
123+
sinkModel(type, path, kind) and
124+
path = "Method[" + method + "]" and
125+
method = this.getMethodName()
126+
// TODO: (type, path) corresponds to this method
127+
)
128+
}
129+
}
130+
131+
/**
132+
* A callable where there exists a MaD source model that applies to it.
133+
*/
134+
class SourceCallable extends DataFlow::CallableNode {
135+
SourceCallable() { sourceElement(this.asExpr().getExpr(), _, _, _) }
136+
}
137+
138+
/**
139+
* A class of effectively public callables from source code.
140+
*/
141+
class PublicEndpointFromSource extends Endpoint {
142+
override predicate isSource() { this instanceof SourceCallable }
143+
144+
override predicate isSink() { this instanceof SinkCallable }
145+
}

0 commit comments

Comments
 (0)