Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Build OSSEC packages
run: |
UBUNTU_VERSION=${{ matrix.versions.ubuntu }} WHAT=ossec ./builder/build-debs.sh
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
id: upload
with:
name: ${{ matrix.versions.ubuntu }}-${{ matrix.build }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/translation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: |
make translation-test
- name: Save screenshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: screenshots-${{ matrix.locale }}
path: securedrop/tests/functional/pageslayout/screenshots/
17 changes: 17 additions & 0 deletions API2.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Note over Client: Global version uvwxyz
end
```

#### Consistency

This diagram implies single-round-trip consistency. To make that expectation
explicit:

Expand All @@ -98,3 +100,18 @@ E_n\}$; and
3. $S$ accepts $BR$ as valid and successfully processes all $E_i$; then

4. $C$'s index SHOULD match $S$'s index without a subsequent synchronization.

#### Snowflake IDs

The `Event.id` field is a "snowflake ID", which a client can generate using a
library like [`@sapphire/snowflake`]. To avoid precision-loss problems:

- A client SHOULD store its IDs as opaque strings and sort them
lexicographically.

- A client MUST encode its IDs on the wire as JSON strings.

- The server MAY convert IDs it receives to integers, but only for sorting and
testing equality.

[`@sapphire/snowflake`]: https://www.npmjs.com/package/@sapphire/snowflake
34 changes: 27 additions & 7 deletions molecule/testinfra/app/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,26 @@
sdvars = testutils.securedrop_test_vars
testinfra_hosts = [sdvars.app_hostname]

_TIMEOUT = 30.0
_POLL_FREQUENCY = 5.0

JOURNALIST_PUB = "/var/lib/securedrop/journalist.pub"
WEAK_KEY_CONTENTS = (
Path(__file__).parent.parent.parent.parent / "redwood/res/weak_sample_key.asc"
).read_text()


def wait_for(necessary_condition, timeout=_TIMEOUT):
"""Polling wait for an arbitrary true/false condition"""
start_time = time.time()
while time.time() - start_time < timeout:
if necessary_condition():
return True
time.sleep(_POLL_FREQUENCY)
# one last chance!
return necessary_condition()


@pytest.mark.parametrize(
("name", "url", "curl_flags", "expected"),
[
Expand All @@ -34,13 +47,13 @@ def test_interface_up(host, name, url, curl_flags, expected):
best to grab the error log and print it via an intentionally failed
assertion.
"""
response = host.run(f"curl -{curl_flags}i {url}").stdout
if "200 OK" not in response:
if not wait_for(lambda: "200 OK" in host.run(f"curl -{curl_flags}i {url}").stdout):
# Try to grab the log and print it via a failed assertion
with host.sudo():
f = host.file(f"/var/log/apache2/{name}-error.log")
if f.exists:
assert "nopenopenope" in f.content_string
response = host.run(f"curl -{curl_flags}i {url}").stdout
assert "200 OK" in response
assert expected in response

Expand Down Expand Up @@ -76,12 +89,19 @@ def test_weak_submission_key(host):
# give the interfaces a chance to come up - a TODO could be polling here
time.sleep(10)
# Now try to hit the JI
response = host.run("curl -Li http://localhost:8080/").stdout
assert "HTTP/1.1 500 Internal Server Error" in response
assert wait_for(
lambda: "HTTP/1.1 500 Internal Server Error"
in host.run("curl -Li http://localhost:8080/").stdout
)
# Now hit the SI
response = host.run("curl -i http://localhost:80/").stdout
assert "HTTP/1.1 503 SERVICE UNAVAILABLE" in response # Flask shouts
assert "We're sorry, our SecureDrop is currently offline." in response
assert wait_for(
lambda: "HTTP/1.1 503 SERVICE UNAVAILABLE"
in host.run("curl -i http://localhost:80/").stdout
)
assert wait_for(
lambda: "We're sorry, our SecureDrop is currently offline"
in host.run("curl -i http://localhost:80/").stdout
)

finally:
set_public_key(host, old_public_key)
Expand Down
13 changes: 10 additions & 3 deletions molecule/testinfra/vars/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,33 @@ log_events_without_ossec_alerts:
rule_id: "100114"

# #6866
- name: NameError_hasattr_does_not_produce_alert
- name: test_NameError_hasattr_does_not_produce_alert
alert: >
NameError: name 'hasattr' is not defined
level: "0"
rule_id: "199996"

# #7491
- name: Update_notifier_download_failed_text_no_alert
- name: test_Update_notifier_download_failed_text_no_alert
alert: >
Download data for packages that failed at package install time
level: "0"
rule_id: "199994"

# #7491
- name: apt_news_warning_text_no_alert
- name: test_apt_news_warning_text_no_alert
alert: >
Warning: W:Download is performed unsandboxed as root as file
level: "0"
rule_id: "199995"

# #7670
- name: test_tor_hop_failure_no_alert
alert: >
Failed to find node for hop #1 of our path. Discarding this circuit.
level: "0"
rule_id: "199997"

# OSSEC should not alert when "manage.py check-disconnected-{db,fs}-
# submissions" has logged that there are no disconnected submissions.
- name: test_no_disconnected_db_submissions_produces_alert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
</group>

<group name="do not alert">
<rule id="199993" level="0">
<match>Failed to find node for hop</match>
<description>ignore non-fatal error generated by Tor</description>
<options>no_email_alert</options>
</rule>

<rule id="199994" level="0">
<match>Download data for packages that failed at package install time</match>
<description>ignore update_notifier_download.service text with "failed" string (https://github.com/freedomofpress/securedrop/issues/7491)</description>
Expand Down
2 changes: 1 addition & 1 deletion securedrop/journalist_app/api2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def data() -> Response:
)

# Process events in snowflake order.
for event in sorted(events, key=lambda e: e.id):
for event in sorted(events, key=lambda e: int(e.id)):
result = handler.process(event)
for uuid, source in result.sources.items():
response.sources[uuid] = source.to_api_v2() if source is not None else None
Expand Down
43 changes: 43 additions & 0 deletions securedrop/tests/test_journalist_api2.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,46 @@ def test_api2_idempotence_period(journalist_app):
"""

assert journalist_app.config["SESSION_LIFETIME"] <= api2.events.IDEMPOTENCE_PERIOD


def test_api2_event_ordering(journalist_app, journalist_api_token, test_files):
"""
If two `item_deleted` events for the same item arrive out of order, the
numerically later event must observe that the item is already gone by the
time it's processed.
"""
with journalist_app.test_client() as app:
index = app.get(
url_for("api2.index"),
headers=get_api_headers(journalist_api_token),
)
assert index.status_code == 200

submission_uuid = test_files["submissions"][0].uuid
item_version = index.json["items"][submission_uuid]

# Two `item_deleted` events targeting the same item:
e2 = Event(
id="3419026047977394171",
target=ItemTarget(item_uuid=submission_uuid, version=item_version),
type=EventType.ITEM_DELETED,
)
e1 = Event(
id="3419026047977394170", # client sends as string; server orders as integer
target=ItemTarget(item_uuid=submission_uuid, version=item_version),
type=EventType.ITEM_DELETED,
)

# Send them out of order:
resp = app.post(
url_for("api2.data"),
json={"events": [asdict(e2), asdict(e1)]},
headers=get_api_headers(journalist_api_token),
)
assert resp.status_code == 200

# Event `1` (sent second, processed first) deletes the item.
assert resp.json["events"]["3419026047977394170"] == [200, None]

# Event "2" (sent first, processed second) finds it missing.
assert resp.json["events"]["3419026047977394171"][0] == 410
Loading