16 | 16 |
|
17 | 17 |
Dependencies:
|
18 | 18 |
$ pip install rich
|
|
19 |
|
|
20 |
Versions:
|
|
21 |
You can specify package version by adding a semicolon to the package name:
|
|
22 |
|
|
23 |
# Tests version 1.6.0 and 1.6.3
|
|
24 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.6.0,1.6.3 doctrine/rce1
|
|
25 |
|
|
26 |
or with a range:
|
|
27 |
|
|
28 |
# Tests from version 5.0.0 to 6.1.3
|
|
29 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.6.0..1.12.3 doctrine/rce1
|
|
30 |
|
|
31 |
If no upper or lower version is present, every version before (resp. after)
|
|
32 |
the specified one will be tested:
|
|
33 |
|
|
34 |
# from doctrine 1.12.0 to the newest
|
|
35 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.12.0.. doctrine/rce1
|
|
36 |
# from the first version of doctrine to 1.6.0
|
|
37 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:..1.6.0 doctrine/rce1
|
|
38 |
|
19 | 39 |
|
20 | 40 |
Credit goes to @M4yFly for the original idea and implementation.
|
21 | 41 |
"""
|
|
28 | 48 |
import tempfile
|
29 | 49 |
import shutil
|
30 | 50 |
|
31 | |
|
32 | 51 |
try:
|
33 | 52 |
from rich import print
|
34 | 53 |
except ImportError:
|
|
57 | 76 |
for gc in self._gcs:
|
58 | 77 |
self.ensure_gc_exists(gc)
|
59 | 78 |
|
60 | |
php_version = self._executor.php("--version")[0].split('\n')[0]
|
61 | |
print(
|
62 | |
f"Running on PHP version "
|
63 | |
f"[blue]{php_version}[/blue]"
|
64 | |
f"."
|
65 | |
)
|
66 | |
|
67 | |
versions = self._package.get_versions()
|
|
79 |
php_version = self._executor.php("--version")[0].split("\n")[0]
|
|
80 |
print(f"Running on PHP version " f"[blue]{php_version}[/blue]" f".")
|
|
81 |
|
|
82 |
versions = self._package.get_target_versions()
|
68 | 83 |
print(
|
69 | 84 |
f"Testing {len(versions)} versions for "
|
70 | 85 |
f"[blue]{self._package.name}[/blue] against "
|
|
137 | 152 |
|
138 | 153 |
def setup_arguments():
|
139 | 154 |
parser = argparse.ArgumentParser(
|
140 | |
description="Test PHPGGC gadget chains against every version of a composer package."
|
|
155 |
description="Test PHPGGC gadget chains against every version of a composer package.",
|
|
156 |
epilog="""\
|
|
157 |
Example:
|
|
158 |
$ ./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3
|
|
159 |
|
|
160 |
Required executables:
|
|
161 |
The program requires phpggc and composer.
|
|
162 |
By default, it will use the `phpggc` from the current directory, and the
|
|
163 |
composer from PATH. If you wish to use other paths, use the `PHPGGC_PATH`
|
|
164 |
and `COMPOSER_PATH` environment variables.
|
|
165 |
If a file cannot be ran straight up, we'll try using `php <file>` instead.
|
|
166 |
|
|
167 |
Dependencies:
|
|
168 |
$ pip install rich
|
|
169 |
|
|
170 |
Versions:
|
|
171 |
You can specify package version by adding a semicolon to the package name:
|
|
172 |
|
|
173 |
# Tests version 1.6.0 and 1.6.3
|
|
174 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.6.0,1.6.3 doctrine/rce1
|
|
175 |
|
|
176 |
or with a range:
|
|
177 |
|
|
178 |
# Tests from version 5.0.0 to 6.1.3
|
|
179 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.6.0..1.12.3 doctrine/rce1
|
|
180 |
|
|
181 |
If no upper or lower version is present, every version before (resp. after)
|
|
182 |
the specified one will be tested:
|
|
183 |
|
|
184 |
# from doctrine 1.12.0 to the newest
|
|
185 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:1.12.0.. doctrine/rce1
|
|
186 |
# from the first version of doctrine to 1.6.0
|
|
187 |
$ ./test-gc-compatibility.py doctrine/doctrine-bundle:..1.6.0 doctrine/rce1
|
|
188 |
""",
|
|
189 |
formatter_class=argparse.RawTextHelpFormatter,
|
141 | 190 |
)
|
142 | 191 |
parser.add_argument("package")
|
143 | 192 |
parser.add_argument("gadget_chain", nargs="+")
|
|
188 | 237 |
work_dir = pathlib.Path(__file__).parent.resolve()
|
189 | 238 |
phpggc = os.environ.get("PHPGGC_PATH", str(work_dir / "phpggc"))
|
190 | 239 |
composer = os.environ.get("COMPOSER_PATH", "composer")
|
191 | |
php = os.environ.get("PHP_PATH", "php")
|
192 | 240 |
|
193 | 241 |
if not pathlib.Path(phpggc).is_file():
|
194 | 242 |
raise TesterException("phpggc executable not found")
|
|
224 | 272 |
"""Represents a composer package."""
|
225 | 273 |
|
226 | 274 |
def __init__(self, name, executor):
|
227 | |
self.name = name
|
|
275 |
self.extract_name_versions(name)
|
228 | 276 |
self._executor = executor
|
229 | 277 |
self.work_dir = pathlib.Path(tempfile.mkdtemp(prefix="phpggc"))
|
230 | 278 |
|
231 | |
def get_versions(self):
|
232 | |
"""Uses composer to obtain each version (or tag) for the package."""
|
233 | |
if ":" in self.name:
|
234 | |
try:
|
235 | |
versions = self.name.split(":")[1]
|
236 | |
self.name = self.name.split(":")[0]
|
237 | |
return [v.strip() for v in versions.split(",")]
|
238 | |
except IndexError:
|
239 | |
raise IndexError("Inexistant index")
|
240 | |
except AttributeError:
|
241 | |
raise AttributeError("No version defined")
|
|
279 |
def extract_name_versions(self, name):
|
|
280 |
if ":" not in name:
|
|
281 |
self.name = name
|
|
282 |
self.versions = None
|
|
283 |
else:
|
|
284 |
self.name, self.versions = name.split(":")
|
|
285 |
|
|
286 |
def get_package_versions(self):
|
242 | 287 |
versions, _ = self._executor.composer("show", "-a", self.name)
|
243 | 288 |
versions = re.search(r"versions :(.*)\ntype", versions).group(1)
|
244 | 289 |
return [v.strip() for v in versions.split(",")]
|
|
290 |
|
|
291 |
def get_target_versions(self):
|
|
292 |
"""Uses composer to obtain each version (or tag) for the package."""
|
|
293 |
if self.versions is None:
|
|
294 |
return self.get_package_versions()
|
|
295 |
|
|
296 |
package_versions = None
|
|
297 |
target_versions = []
|
|
298 |
|
|
299 |
def get_version_idx_or_raise(version):
|
|
300 |
try:
|
|
301 |
return package_versions.index(version)
|
|
302 |
except ValueError:
|
|
303 |
raise ValueError(f"Version {version} could not be found")
|
|
304 |
|
|
305 |
for version in self.versions.split(","):
|
|
306 |
# range
|
|
307 |
if ".." in version:
|
|
308 |
vmin, vmax = version.split("..")
|
|
309 |
if package_versions is None:
|
|
310 |
package_versions = self.get_package_versions()
|
|
311 |
|
|
312 |
vmin_idx = (
|
|
313 |
get_version_idx_or_raise(vmin) if vmin else len(package_versions)
|
|
314 |
)
|
|
315 |
vmax_idx = get_version_idx_or_raise(vmax) if vmax else 0
|
|
316 |
# Versions are stored from biggest to smallest
|
|
317 |
target_versions += package_versions[vmax_idx : vmin_idx + 1]
|
|
318 |
else:
|
|
319 |
target_versions.append(version)
|
|
320 |
|
|
321 |
return target_versions
|
245 | 322 |
|
246 | 323 |
def clean_workdir(self, final=False):
|
247 | 324 |
"""Removes any composer related file in the working directory, such as
|