Skip to content

Commit 48968f6

Browse files
Merge pull request #15 from AlexanderGrooff/nested-includes
2 parents 9bb540b + c9cc177 commit 48968f6

File tree

11 files changed

+160
-63
lines changed

11 files changed

+160
-63
lines changed

.github/workflows/test.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ jobs:
1212
- uses: actions/checkout@v3
1313
- run: sudo python setup.py install
1414
- run: nginx-static-analysis --help
15-
integration_test:
15+
unit_test:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- uses: actions/checkout@v3
19-
- name: Run general testsuite
20-
run: ./runtests.sh
21-
shell: bash
19+
- uses: actions/setup-python@v3
20+
- run: pip install -r requirements/development.txt
21+
- run: coverage run -m pytest
22+
- run: coverage report
23+
- run: coverage xml -o ./coverage.xml
2224
- name: Upload coverage reports
2325
uses: codecov/codecov-action@v3
2426
with:
2527
files: ./coverage.xml
28+
integration_test:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v3
32+
- name: Run general testsuite
33+
run: ./runtests.sh
34+
shell: bash
2635
linter:
2736
runs-on: ubuntu-latest
2837
steps:

example_nginx/servers/example.com.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ server {
88
location / {
99
proxy_pass http://localhost:8003;
1010
}
11+
include /etc/nginx/servers/include/*.conf;
1112
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
location /nested {
2+
return 418;
3+
}

nginx_analysis/analysis.py

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
RootNginxConfig,
1010
filter_unique,
1111
get_children_recursive,
12+
get_parents_recursive,
1213
)
1314

1415

1516
def nginx_to_regex(nginx: str) -> str:
1617
"""
17-
Convert a nginx-style regex to a python regex
18+
Convert a nginx-style regex to a python regex.
19+
Wildcard files are converted to regexes that match any file in the directory.
1820
"""
19-
return nginx.replace("*", ".*")
21+
return nginx.replace("*", r"[^/]*")
2022

2123

2224
def set_parents_in_include(root_config: RootNginxConfig, block_config: NginxLineConfig):
@@ -27,6 +29,7 @@ def set_parents_in_include(root_config: RootNginxConfig, block_config: NginxLine
2729
for nested_line_config in nested_file_config.parsed:
2830
nested_line_config.parent = block_config
2931
block_config.children.append(nested_line_config)
32+
set_parents_in_include(root_config, nested_line_config)
3033

3134

3235
def set_parents_of_blocks(root_config: RootNginxConfig, line_config: NginxLineConfig):
@@ -104,44 +107,62 @@ def is_partial_direct_match(
104107
return None
105108

106109

107-
def find_matches_in_children(
110+
def get_matching_lines_in_children(
108111
line: NginxLineConfig, filters: List[DirectiveFilter]
109112
) -> Tuple[List[NginxLineConfig], List[DirectiveFilter]]:
110113
"""
111-
Check if the line matches filters. If it matches, return the line,
112-
the line's neighbours and all children. If it doesn't match, check
113-
if any of the children match the filters. If they do, return all
114-
descendants of the line that match the filters, including the current
115-
line.
114+
Check if the line matches filters. If it matches, return the line.
115+
If it doesn't match, check if any of the children match the filters.
116116
We also return a list of filters that matched, so we can check if
117117
all filters were matched eventually.
118118
If there are no matches, return an empty list for both the matching
119119
lines and the matched filters.
120120
"""
121+
if not filters:
122+
# No filters, meaning that the line matches directly
123+
return [line], []
124+
125+
logger.debug(f"Checking if line {line} matches filters: {filters}")
121126
matched_filter = is_partial_direct_match(line, filters)
122127
if matched_filter:
123128
logger.debug(f"Found match in children: {line}")
124129
all_matched_filters = set([matched_filter])
125130
# Search for remaining filters in children, as the parent
126131
# might still be looking for other filters
127-
remaining_filters = [f for f in filters if f != matched_filter]
128132
for child in line.children:
129-
_, child_matched_filters = find_matches_in_children(
130-
child, remaining_filters
131-
)
132-
all_matched_filters.update(child_matched_filters)
133-
return [line, *line.neighbours, *get_children_recursive(line)], list(
134-
all_matched_filters
135-
)
133+
remaining_filters = [f for f in filters if f not in all_matched_filters]
134+
if remaining_filters:
135+
logger.debug(
136+
f"Looking for remaining filters in child {child}: {remaining_filters}"
137+
)
138+
_, child_matched_filters = get_matching_lines_in_children(
139+
child, remaining_filters
140+
)
141+
all_matched_filters.update(child_matched_filters)
142+
else:
143+
logger.debug(
144+
f"Found all filters in children of line {line}. We should stop!"
145+
)
146+
break
147+
return [line], list(all_matched_filters)
136148

137149
# All filters must match at least one child
138150
matched_filters = set()
139-
matches = [line]
151+
matches = []
140152
for child in line.children:
141-
child_matches, child_matched_filters = find_matches_in_children(child, filters)
142-
if child_matches:
143-
matches.extend(child_matches)
144-
matched_filters.update(child_matched_filters)
153+
remaining_filters = [f for f in filters if f not in matched_filters]
154+
if remaining_filters:
155+
child_matches, child_matched_filters = get_matching_lines_in_children(
156+
child, remaining_filters
157+
)
158+
if child_matches:
159+
matches.extend(child_matches)
160+
matched_filters.update(child_matched_filters)
161+
else:
162+
logger.debug(
163+
f"Found all filters in children of line {line}. We should stop!"
164+
)
165+
break
145166

146167
if len(matched_filters) == len(filters):
147168
logger.debug(
@@ -151,6 +172,25 @@ def find_matches_in_children(
151172
return [], []
152173

153174

175+
def expand_upon_direct_match(
176+
match: NginxLineConfig, matched_filters: List[DirectiveFilter]
177+
) -> List[NginxLineConfig]:
178+
"""
179+
Expand upon matching lines by recursively looking through
180+
parents and neighbours. However, we don't want to include any
181+
lines that have the same directive as in the filters.
182+
F.e., if the filter looks for location=/banaan, we don't want
183+
to include other recursive location lines.
184+
"""
185+
matching_lines = []
186+
for neighbour in match.neighbours:
187+
matching_lines.append(neighbour)
188+
matching_lines.extend(get_children_recursive(neighbour))
189+
parents = get_parents_recursive(match)
190+
matching_lines.extend(parents)
191+
return matching_lines
192+
193+
154194
def filter_config(
155195
lines: Iterator[NginxLineConfig],
156196
filters: List[DirectiveFilter],
@@ -160,10 +200,10 @@ def filter_config(
160200
"""
161201
matching_lines = []
162202
for line in lines:
163-
child_matches, matched_filters = find_matches_in_children(line, filters)
164-
if len(matched_filters) == len(filters):
165-
logger.debug(f"Matched all ({len(matched_filters)}) filters: {line}")
166-
matching_lines.extend(child_matches)
203+
child_matches, _ = get_matching_lines_in_children(line, filters)
204+
matching_lines.extend(child_matches)
205+
for match in child_matches:
206+
matching_lines.extend(expand_upon_direct_match(match, filters))
167207

168208
return filter_unique(matching_lines)
169209

nginx_analysis/dataclasses.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ def match(self, line: "NginxLineConfig") -> bool:
6666
def __hash__(self) -> int:
6767
return hash(str(self))
6868

69-
def __str__(self) -> str:
69+
def __repr__(self) -> str:
7070
return f"{self.directive} -> {self.value}"
7171

72+
def __str__(self) -> str:
73+
return self.__repr__()
74+
7275

7376
class NginxLineConfig(BaseModel):
7477
directive: str
@@ -103,9 +106,12 @@ def __eq__(self, other: Any) -> bool:
103106
def __hash__(self) -> int:
104107
return hash(str(self))
105108

106-
def __str__(self) -> str:
109+
def __repr__(self) -> str:
107110
return f"{self.file}:{self.line} -> {self.directive}"
108111

112+
def __str__(self) -> str:
113+
return self.__repr__()
114+
109115

110116
class NginxFileConfig(BaseModel):
111117
file: Path
@@ -121,9 +127,12 @@ def __eq__(self, other: Any) -> bool:
121127
comparison_fields = ["file", "status", "errors", "parsed"]
122128
return compare_objects(self, other, comparison_fields)
123129

124-
def __str__(self) -> str:
130+
def __repr__(self) -> str:
125131
return f"{self.file}"
126132

133+
def __str__(self) -> str:
134+
return self.__repr__()
135+
127136
@property
128137
def lines(self) -> Generator[NginxLineConfig, None, None]:
129138
for line in self.parsed:
@@ -183,3 +192,9 @@ def __eq__(self, other: Any) -> bool:
183192

184193
comparison_fields = ["status", "errors", "config"]
185194
return compare_objects(self, other, comparison_fields)
195+
196+
def __repr__(self) -> str:
197+
return f"{self.status} {self.root_file}"
198+
199+
def __str__(self) -> str:
200+
return self.__repr__()

runtests.sh

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,4 @@ if [[ ! $(docker-compose ps --services --filter status=running nginx | grep ngin
1616
docker-compose up -d nginx
1717
fi
1818

19-
if [[ ! $($NGINX_EXEC command -v pytest) ]]; then
20-
$NGINX_EXEC python3 -m pip install pytest
21-
fi
22-
23-
$NGINX_EXEC coverage run -m pytest
24-
$NGINX_EXEC coverage report
25-
$NGINX_EXEC coverage xml -o coverage.xml
2619
$NGINX_EXEC ./scripts/integration_tests.sh

scripts/integration_tests.sh

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,35 @@ trap uninstall EXIT
1313
python setup.py install
1414
NSA=/usr/local/bin/nginx-static-analysis
1515

16-
# Ensure the basic functionality works
16+
echo "Ensure the basic functionality works"
1717
$NSA --help || (echo "NOT OK" && exit 1)
1818
$NSA -d server_name || (echo "NOT OK" && exit 1)
19-
# Check if only unique lines are returned
19+
20+
echo "Check if only unique lines are returned"
2021
test $($NSA -d server_name |& grep example.com | wc -l) = 1 || (echo "NOT OK" && exit 1)
21-
# Garbage in --> no matches
22+
23+
echo "Garbage in --> no matches"
2224
$NSA -d blabla |& grep "Found no matches" || (echo "NOT OK" && exit 1)
23-
# Value should show the value in table format
25+
26+
echo "Value should show the value in table format"
2427
$NSA -d server_name -f server_name=example.com |& grep example.com || (echo "NOT OK" && exit 1)
25-
# Value should not show other values
28+
29+
echo "Value should not show other values"
2630
$NSA -d server_name -f server_name=banaan.com |& grep example.com && (echo "NOT OK" && exit 1)
27-
# Multiple filters should show the most specific match
31+
32+
echo "Multiple filters should show the most specific match"
2833
$NSA -f location=/ -f server_name=example.com |& grep "/etc/nginx/servers/example.com.conf:8" || (echo "NOT OK" && exit 1)
29-
# Show directives next to the direct match
34+
35+
echo "Show directives next to the direct match"
3036
$NSA -f server_name=example.com |& grep "listen" | grep -v "default_server" || (echo "NOT OK" && exit 1)
3137
$NSA -f server_name=testalex.hypernode.io |& grep "listen" | grep "default_server" || (echo "NOT OK" && exit 1)
32-
# Don't show nested children of neighbours of direct match
33-
$NSA -f server_name=testalex.hypernode.io -f location=/ |& grep "404" && (echo "NOT OK" && exit 1)
38+
39+
echo "Don't show nested children of parent neighbours of direct match"
40+
$NSA -f server_name=testalex.hypernode.io |& grep "/etc/nginx/servers/example.com.conf" && (echo "NOT OK" && exit 1)
41+
$NSA -f server_name=example.com |& grep "/etc/nginx/servers/testalex.hypernode.io.conf" && (echo "NOT OK" && exit 1)
42+
43+
echo "Show nested includes"
44+
$NSA -f server_name=testalex.hypernode.io |& grep "/nested" && (echo "NOT OK" && exit 1)
45+
$NSA -f server_name=example.com |& grep "/nested" || (echo "NOT OK" && exit 1)
3446

3547
echo "All tests passed"

tests/unit/analysis/test_find_matches_in_children.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from nginx_analysis.analysis import find_matches_in_children
1+
from nginx_analysis.analysis import get_matching_lines_in_children
22
from nginx_analysis.dataclasses import DirectiveFilter, NginxLineConfig
33
from tests.testcase import TestCase
44

@@ -11,8 +11,12 @@ def test_return_empty_list_if_no_children_and_no_match(self):
1111
line=1,
1212
)
1313
dfilter = DirectiveFilter(directive="location", value="/")
14-
self.assertEqual(find_matches_in_children(line, filters=[dfilter]), ([], []))
15-
self.assertEqual(find_matches_in_children(line, filters=[dfilter]), ([], []))
14+
self.assertEqual(
15+
get_matching_lines_in_children(line, filters=[dfilter]), ([], [])
16+
)
17+
self.assertEqual(
18+
get_matching_lines_in_children(line, filters=[dfilter]), ([], [])
19+
)
1620

1721
def test_return_empty_list_if_args_dont_match(self):
1822
line = NginxLineConfig(
@@ -21,8 +25,12 @@ def test_return_empty_list_if_args_dont_match(self):
2125
line=1,
2226
)
2327
dfilter = DirectiveFilter(directive="return", value="123")
24-
self.assertEqual(find_matches_in_children(line, filters=[dfilter]), ([], []))
25-
self.assertEqual(find_matches_in_children(line, filters=[dfilter]), ([], []))
28+
self.assertEqual(
29+
get_matching_lines_in_children(line, filters=[dfilter]), ([], [])
30+
)
31+
self.assertEqual(
32+
get_matching_lines_in_children(line, filters=[dfilter]), ([], [])
33+
)
2634

2735
def test_return_line_if_direct_match(self):
2836
line = NginxLineConfig(
@@ -32,7 +40,7 @@ def test_return_line_if_direct_match(self):
3240
)
3341
dfilter = DirectiveFilter(directive="return", value="404")
3442
self.assertEqual(
35-
find_matches_in_children(line, filters=[dfilter]), ([line], [dfilter])
43+
get_matching_lines_in_children(line, filters=[dfilter]), ([line], [dfilter])
3644
)
3745

3846
def test_return_line_if_direct_match_for_one_filter(self):
@@ -44,11 +52,11 @@ def test_return_line_if_direct_match_for_one_filter(self):
4452
dfilter1 = DirectiveFilter(directive="return", value="404")
4553
dfilter2 = DirectiveFilter(directive="return", value="405")
4654
self.assertEqual(
47-
find_matches_in_children(line, filters=[dfilter1, dfilter2]),
55+
get_matching_lines_in_children(line, filters=[dfilter1, dfilter2]),
4856
([line], [dfilter1]),
4957
)
5058

51-
def test_return_parent_and_children_if_match_child(self):
59+
def test_return_child_if_match_child(self):
5260
parent = NginxLineConfig(
5361
directive="location",
5462
args=["/"],
@@ -64,11 +72,11 @@ def test_return_parent_and_children_if_match_child(self):
6472

6573
dfilter = DirectiveFilter(directive="return", value="404")
6674
self.assertEqual(
67-
find_matches_in_children(parent, filters=[dfilter]),
68-
([parent, line], [dfilter]),
75+
get_matching_lines_in_children(parent, filters=[dfilter]),
76+
([line], [dfilter]),
6977
)
7078

71-
def test_return_parent_and_children_if_match_parent(self):
79+
def test_return_parent_if_match_parent(self):
7280
parent = NginxLineConfig(
7381
directive="location",
7482
args=["/"],
@@ -84,6 +92,6 @@ def test_return_parent_and_children_if_match_parent(self):
8492

8593
dfilter = DirectiveFilter(directive="location", value="/")
8694
self.assertEqual(
87-
find_matches_in_children(parent, filters=[dfilter]),
88-
([parent, line], [dfilter]),
95+
get_matching_lines_in_children(parent, filters=[dfilter]),
96+
([parent], [dfilter]),
8997
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import re
2+
3+
from nginx_analysis.analysis import nginx_to_regex
4+
from tests.testcase import TestCase
5+
6+
7+
class TestNginxToRegex(TestCase):
8+
def test_regular_string_doesnt_change(self):
9+
ret = nginx_to_regex("test")
10+
self.assertEqual(ret, "test")
11+
12+
def test_wildcard_only_matches_that_directory(self):
13+
regex = nginx_to_regex("*.conf")
14+
self.assertTrue(re.match(regex, "test.conf"))
15+
self.assertFalse(re.match(regex, "test/test.conf"))

0 commit comments

Comments
 (0)