|
11 | 11 |
|
12 | 12 | import pytest |
13 | 13 |
|
14 | | -from socketsecurity.core.classes import Comment, Diff, Issue |
| 14 | +from socketsecurity.core import Core |
| 15 | +from socketsecurity.core.classes import Comment, Diff, Issue, Package |
15 | 16 | from socketsecurity.core.messages import Messages |
16 | 17 | from socketsecurity.core.scm_comments import Comments |
17 | 18 |
|
@@ -503,3 +504,129 @@ def test_plain_style_is_left_verbatim(self): |
503 | 504 | manifest_str, _ = Messages.create_sources(alert, "plain") |
504 | 505 |
|
505 | 506 | assert "<img src=x>/package.json" in manifest_str |
| 507 | + |
| 508 | + |
| 509 | +class TestIntroducedBy: |
| 510 | + """The default comment names the direct dependency behind a transitive alert.""" |
| 511 | + |
| 512 | + def _body(self, alert: Issue) -> str: |
| 513 | + return Messages.security_comment_template(_make_diff([alert]), _FakeConfig()) |
| 514 | + |
| 515 | + def test_transitive_alert_names_its_ancestor(self): |
| 516 | + alert = _make_alert( |
| 517 | + manifests="", |
| 518 | + introduced_by=[("npm/@acme/widgets@1.2.0", "package.json")], |
| 519 | + ) |
| 520 | + |
| 521 | + body = self._body(alert) |
| 522 | + |
| 523 | + assert ( |
| 524 | + "<p><strong>Introduced by:</strong> <code>npm/@acme/widgets@1.2.0</code></p>" |
| 525 | + in body |
| 526 | + ) |
| 527 | + assert_html_block_intact(body) |
| 528 | + |
| 529 | + def test_direct_dependency_gets_no_line(self): |
| 530 | + alert = _make_alert(manifests="", introduced_by=[("direct", "package.json")]) |
| 531 | + |
| 532 | + body = self._body(alert) |
| 533 | + |
| 534 | + assert "Introduced by" not in body |
| 535 | + assert_html_block_intact(body) |
| 536 | + |
| 537 | + def test_transitive_without_known_ancestors_gets_no_line(self): |
| 538 | + body = self._body(_make_alert()) |
| 539 | + |
| 540 | + assert "Introduced by" not in body |
| 541 | + assert_html_block_intact(body) |
| 542 | + |
| 543 | + def test_repeated_ancestors_are_listed_once(self): |
| 544 | + """One ancestor declared in two manifests yields two entries.""" |
| 545 | + alert = _make_alert( |
| 546 | + manifests="", |
| 547 | + introduced_by=[ |
| 548 | + ("npm/express@4.0.0", "a/package.json"), |
| 549 | + ("npm/express@4.0.0", "b/package.json"), |
| 550 | + ], |
| 551 | + ) |
| 552 | + |
| 553 | + assert Messages.introduced_by_html(alert).count("npm/express@4.0.0") == 1 |
| 554 | + |
| 555 | + def test_long_ancestor_lists_are_capped(self): |
| 556 | + limit = Messages.INTRODUCED_BY_LIMIT |
| 557 | + alert = _make_alert( |
| 558 | + manifests="", |
| 559 | + introduced_by=[(f"npm/root-{i}@1.0.0", "package.json") for i in range(limit + 3)], |
| 560 | + ) |
| 561 | + |
| 562 | + html = Messages.introduced_by_html(alert) |
| 563 | + |
| 564 | + assert html.count("<code>") == limit |
| 565 | + assert html.endswith(" and 3 more</p>") |
| 566 | + |
| 567 | + def test_ancestor_name_is_escaped(self): |
| 568 | + alert = _make_alert( |
| 569 | + manifests="", |
| 570 | + introduced_by=[("npm/<img src=x>@1.0.0", "package.json")], |
| 571 | + ) |
| 572 | + |
| 573 | + body = self._body(alert) |
| 574 | + |
| 575 | + assert "<img src=x>" not in body |
| 576 | + assert "npm/<img src=x>@1.0.0" in body |
| 577 | + |
| 578 | + def test_license_row_names_its_ancestor(self): |
| 579 | + alert = _make_alert( |
| 580 | + type="licenseSpdxDisj", |
| 581 | + title="License Policy Violation", |
| 582 | + manifests="", |
| 583 | + introduced_by=[("pypi/requests@2.31.0", "requirements.txt")], |
| 584 | + ) |
| 585 | + |
| 586 | + body = self._body(alert) |
| 587 | + |
| 588 | + assert "has a License Policy Violation" in body |
| 589 | + assert "<code>pypi/requests@2.31.0</code>" in body |
| 590 | + assert_html_block_intact(body) |
| 591 | + |
| 592 | + |
| 593 | +def _package(package_id: str, **overrides): |
| 594 | + fields = dict( |
| 595 | + type="npm", |
| 596 | + namespace=None, |
| 597 | + name="widget", |
| 598 | + version="1.0.0", |
| 599 | + id=package_id, |
| 600 | + score={}, |
| 601 | + alerts=[], |
| 602 | + direct=False, |
| 603 | + topLevelAncestors=[], |
| 604 | + manifestFiles=[], |
| 605 | + purl="", |
| 606 | + ) |
| 607 | + fields.update(overrides) |
| 608 | + return Package(**fields) |
| 609 | + |
| 610 | + |
| 611 | +def test_ancestor_labels_keep_their_namespace(): |
| 612 | + """Two scoped npm roots with the same name, and a Maven root, must render as |
| 613 | + distinct, correctly named ancestors in the comment.""" |
| 614 | + packages = { |
| 615 | + "one": _package("one", namespace="@one", manifestFiles=[{"file": "package.json"}]), |
| 616 | + "two": _package("two", namespace="@two", manifestFiles=[{"file": "package.json"}]), |
| 617 | + "mvn": _package( |
| 618 | + "mvn", type="maven", namespace="com.acme", name="core", |
| 619 | + manifestFiles=[{"file": "pom.xml"}], |
| 620 | + ), |
| 621 | + "plain": _package("plain", name="express", manifestFiles=[{"file": "package.json"}]), |
| 622 | + } |
| 623 | + child = _package("child", name="leaf", topLevelAncestors=["one", "two", "mvn", "plain"]) |
| 624 | + |
| 625 | + sources = [source for source, _ in Core.get_source_data(child, packages)] |
| 626 | + |
| 627 | + assert sources == [ |
| 628 | + "npm/@one/widget@1.0.0", |
| 629 | + "npm/@two/widget@1.0.0", |
| 630 | + "maven/com.acme/core@1.0.0", |
| 631 | + "npm/express@1.0.0", |
| 632 | + ] |
0 commit comments