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 .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
requirements_test_pre_commit.txt linguist-generated=true
script/hassfest/docker/Dockerfile linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true merge=ours
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ updates:
ignore:
# Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump.
- dependency-name: "github/gh-aw-actions/**"
- dependency-name: "github/gh-aw-actions"
313 changes: 235 additions & 78 deletions .github/workflows/check-requirements.lock.yml

Large diffs are not rendered by default.

112 changes: 110 additions & 2 deletions .github/workflows/check-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ Read the JSON directly for the full schema. Key fields:
with `status` of `pass`/`warn`/`fail`/`needs_agent` and `details`).
- `rendered_comment` contains, for each `needs_agent` check, two
placeholders to replace:
- `{{CHECK_CELL:<pkg>:<kind>}}` → exactly one of `✅`, `⚠️`, `❌`.
- `{{CHECK_CELL:<pkg>:<kind>}}` → exactly one of `✅`, `☑️`, `⚠️`, `❌`. The
**`security`** check kind uses `☑️` instead of `✅` for the success
case — see its section below for why.
- `{{CHECK_DETAIL:<pkg>:<kind>}}` → `<icon> <one-line explanation>`
(the bullet's `- **<label>**:` prefix is already rendered; replace
only the placeholder).
Expand Down Expand Up @@ -252,11 +254,117 @@ blocking.
by this version). Cite the offending `<file>:<line>` as a clickable
link on the repo host.

### Check kind: `security`

**Baseline** scan of the upstream source for obvious supply-chain red
flags — a cheap first pass, **not** a security review or malware audit.
A clean result means "nothing obvious stood out", not "this package is
safe". The success icon is `☑️` — **never** `✅` — so a passing scan is
not read as an endorsement.

If `repo_public` resolves to ❌ for the same package, mark `security`'s
cell and detail as `—` and explain `Skipped because the source
repository is not publicly accessible.` — the source cannot be fetched.

**Step 1 — Fetch a representative slice**

Locate the source from `package.repo_url`.

- GitHub: resolve the default branch (`GET /repos/{owner}/{repo}`), list
the tree (`GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1`),
find the module dir (`{name}/` or `src/{name}/`, normalising `-` ↔ `_`).
- GitLab: equivalent REST calls. Other hosts: `web-fetch` raw file URLs.

Fetch the **raw contents** of `setup.py` (install-time code runs on every
consumer), `pyproject.toml` (`[build-system]` / custom backend), the
package's `__init__.py`, and co — prioritising `entry_points` targets, plus any name suggesting
bootstrap / loader / self-update (`update*.py`, `loader*.py`,
`bootstrap*.py`, `_native.py`, `_post_install*.py`, …).

If the tree is too large for the API budget, inspect at least `setup.py`,
`pyproject.toml`, and `__init__.py`, then return ⚠️ noting the partial scan.

**Step 2 — Patterns to flag**

Reason from principles, not a fixed checklist: for each file ask *would a
well-behaved library doing what this package's PyPI description claims
need to do this?* If "no" or "unclear", record a finding. The categories
describe the **shape** of concerning behavior; the named APIs, filenames,
and keys are illustrative — treat any equivalent construct (including ones
that did not exist when this was written) the same way.

For every finding include the file path, line number, a snippet
(≤ 120 chars), a permalink
(`https://github.com/{owner}/{repo}/blob/{sha}/{path}#L{line}` or the
GitLab equivalent), and one sentence on why it is out of scope.

1. **Reaches into Home Assistant internals.** A library should touch HA
only through its documented Python API — never the `config_dir`
filesystem or internal auth / session state. Flag code that opens,
reads, writes, or resolves paths to artifacts it does not own
(top-level YAML it did not create, anything under `.storage/`, other
integrations' files) or reads tokens / refresh tokens / auth providers
(e.g. `secrets.yaml`, `.storage/auth*`, `hass.auth`). The principle is
*out-of-scope access*, not a static list of names.
2. **Network input flows into an execution sink (download-and-execute).**
Flag any data-flow from a network response body (any HTTP / WebSocket /
raw-socket client, sync or async) to an execution sink: `exec`, `eval`,
`compile`, `marshal.loads`, `pickle.loads`, `types.FunctionType`,
`importlib.util.spec_from_loader`, `subprocess.*`, `os.system`, shell
pipelines (`curl … | sh`), or a file later imported / executed — plus
package-manager calls (`pip install` / `download`) with args resolved
from network responses at runtime.
3. **Build / install-time code is non-deterministic or non-local.**
`setup.py`, `setup.cfg` `cmdclass`, custom PEP 517 backends, and other
build hooks must only compile and copy files shipped in the sdist. Flag
build-stage code that opens a socket, shells out, writes outside the
build / install tree, or pulls a build backend not on PyPI (Git URL /
local path).
4. **Reads secrets and combines them with an egress path.** The shape is
*secret-source → outbound-channel*. Flag code that reads credential
material (token-like env vars, credential files under the user's home,
OS keychain APIs, browser-profile dirs, HA token stores) **and** in the
same path sends it to a destination the package needn't talk to.
Reading or sending alone is not enough — the *combination* is the signal.
5. **Hides what it does.** Flag opaque data flowing into an execution
sink: large encoded / compressed / hex strings (`base64`, `codecs`,
`zlib`, `lzma`, `bytes.fromhex`, or any equivalent) passed to `exec` /
`eval` / `compile` / `__import__`; identifiers assembled at runtime
then imported; or any construct whose evident purpose is to make the
behavior unreadable.
6. **Hard-coded network destination off-purpose.** Flag outbound URLs or
hosts absent from the package's PyPI `project_urls` with no obvious
connection to its function — short-link / paste services, ephemeral
tunnels, raw IPs, non-default ports against unknown hosts — and any
network call at module top-level / `__init__.py` (runs on import for
every consumer).

A clearly out-of-scope behavior that fits none of the above: flag under
the closest category and explain. The categories guide reasoning, not bound it.

**Verdict**

Aggregate the findings into one of:

- `☑️ Baseline scan found nothing obvious in <list of inspected files>.
This is not a security review — only the cheap checks were run.`
Use `☑️` (**not** `✅`) so a passing scan is not read as an endorsement.
- `⚠️ <one-line summary>` — patterns with plausible legitimate uses;
include path / line / snippet / permalink per match for the reviewer.
- `❌ <one-line summary>` — patterns with no legitimate explanation
(install-time network execution, decode-and-exec of opaque blobs, reads
of `secrets.yaml` / `.storage/auth*`, token exfiltration to an external
host); same detail.

Be precise. False positives are expected — when in doubt prefer `⚠️` with
context over `❌`. This check is informational and never blocks the
workflow on its own; a human reviewer decides whether to merge.

## Notes

- Be constructive; reference the inspected file by URL when useful.
- Comment dedup is handled by gh-aw's `add_comment` safe-output via
the `<!-- requirements-check -->` marker.
- If `/tmp/gh-aw/deterministic/results.json` is missing (upstream
cancelled/failed), emit nothing — the post-step verification is
gated and won't complain.
gated and won't complain.
2 changes: 2 additions & 0 deletions homeassistant/components/blebox/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def __init__(
"""Initialize a BleBox binary sensor feature."""
super().__init__(coordinator, feature)
self.entity_description = description
if feature.name:
self._attr_name = feature.name

@property
def is_on(self) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/blebox/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ async def async_setup_entry(
class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity):
"""Representation of BleBox buttons."""

_attr_name = None

def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.button.Button
) -> None:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/blebox/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async def async_setup_entry(
class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity):
"""Representation of a BleBox climate feature (saunaBox)."""

_attr_name = None
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/blebox/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ async def async_setup_entry(
class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
"""Representation of a BleBox cover feature."""

_attr_name = None

def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.cover.Cover
) -> None:
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/blebox/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
class BleBoxEntity[_FeatureT: Feature](CoordinatorEntity[BleBoxCoordinator]):
"""Implements a common class for entities representing a BleBox feature."""

_attr_has_entity_name = True

def __init__(self, coordinator: BleBoxCoordinator, feature: _FeatureT) -> None:
"""Initialize a BleBox entity."""
super().__init__(coordinator)
self._feature = feature
self._attr_name = feature.full_name
self._attr_unique_id = feature.unique_id
product = feature.product
self._attr_device_info = DeviceInfo(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/blebox/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ def __init__(
super().__init__(coordinator, feature)
if feature.effect_list:
self._attr_supported_features = LightEntityFeature.EFFECT
if feature.index is not None:
self._attr_translation_key = "channel"
self._attr_translation_placeholders = {"index": str(feature.index + 1)}
else:
self._attr_name = None

@property
def is_on(self) -> bool:
Expand Down
32 changes: 29 additions & 3 deletions homeassistant/components/blebox/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""BleBox sensor entities."""

from collections import Counter
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
Expand Down Expand Up @@ -67,6 +68,7 @@ class BleBoxSensorEntityDescription(SensorEntityDescription):
),
BleBoxSensorEntityDescription(
key="temperature",
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
Expand Down Expand Up @@ -97,48 +99,56 @@ class BleBoxSensorEntityDescription(SensorEntityDescription):
),
BleBoxSensorEntityDescription(
key="forwardActiveEnergy",
translation_key="forward_active_energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
BleBoxSensorEntityDescription(
key="reverseActiveEnergy",
translation_key="reverse_active_energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
BleBoxSensorEntityDescription(
key="reactivePower",
device_class=SensorDeviceClass.POWER,
translation_key="reactive_power",
device_class=SensorDeviceClass.REACTIVE_POWER,
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="activePower",
translation_key="active_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="apparentPower",
translation_key="apparent_power",
device_class=SensorDeviceClass.APPARENT_POWER,
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="voltage",
translation_key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="current",
translation_key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="frequency",
translation_key="frequency",
device_class=SensorDeviceClass.FREQUENCY,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
Expand Down Expand Up @@ -172,10 +182,20 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""

coordinator = config_entry.runtime_data
features = coordinator.box.features.get("sensors", [])
counts = Counter(f.device_class for f in features)
entities = [
BleBoxSensorEntity(coordinator, feature, description)
for feature in coordinator.box.features.get("sensors", [])
BleBoxSensorEntity(
coordinator,
feature,
description,
feature.index
if counts[feature.device_class] > 1 and feature.index
else None,
)
for feature in features
for description in SENSOR_TYPES
if description.key == feature.device_class
]
Expand All @@ -192,10 +212,16 @@ def __init__(
coordinator: BleBoxCoordinator,
feature: blebox_uniapi.sensor.BaseSensor,
description: BleBoxSensorEntityDescription,
index: int | None = None,
) -> None:
"""Initialize a BleBox sensor feature."""
super().__init__(coordinator, feature)
self.entity_description = description
if feature.name:
self._attr_name = feature.name
elif index is not None and description.translation_key:
self._attr_translation_key = f"{description.translation_key}_n"
self._attr_translation_placeholders = {"index": str(index)}

@property
def native_value(self) -> StateType:
Expand Down
29 changes: 28 additions & 1 deletion homeassistant/components/blebox/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,24 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "The IP address of your BleBox device.",
"password": "The password for your BleBox device.",
"port": "The port of your BleBox device.",
"username": "The username for your BleBox device."
},
"description": "Set up your BleBox to integrate with Home Assistant.",
"title": "Set up your BleBox device"
}
}
},
"entity": {
"light": { "channel": { "name": "Channel {index}" } },
"sensor": {
"active_power": { "name": "Active power" },
"active_power_n": { "name": "Active power {index}" },
"apparent_power": { "name": "Apparent power" },
"apparent_power_n": { "name": "Apparent power {index}" },
"co2_level": {
"name": "Carbon dioxide level",
"state": {
Expand All @@ -49,15 +60,31 @@
"unhealthy": "Unhealthy"
}
},
"current": { "name": "Current" },
"current_n": { "name": "Current {index}" },
"forward_active_energy": { "name": "Forward active energy" },
"forward_active_energy_n": { "name": "Forward active energy {index}" },
"frequency": { "name": "Frequency" },
"frequency_n": { "name": "Frequency {index}" },
"open_status": {
"name": "Open status",
"state": {
"ajar": "Ajar",
"closed": "[%key:common::state::closed%]",
"closed_but_unlocked": "Closed but unlocked",
"open": "[%key:common::state::open%]",
"unclosed_or_unlocked": "Unclosed or unlocked"
}
}
},
"power_consumption": { "name": "Energy last hour" },
"reactive_power": { "name": "Reactive power" },
"reactive_power_n": { "name": "Reactive power {index}" },
"reverse_active_energy": { "name": "Reverse active energy" },
"reverse_active_energy_n": { "name": "Reverse active energy {index}" },
"temperature": { "name": "Temperature" },
"temperature_n": { "name": "Temperature {index}" },
"voltage": { "name": "Voltage" },
"voltage_n": { "name": "Voltage {index}" }
}
},
"exceptions": {
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/blebox/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import BleBoxConfigEntry
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
from .util import blebox_command

Expand All @@ -34,6 +35,16 @@ class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity

_attr_device_class = SwitchDeviceClass.SWITCH

_attr_name = None

def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.switch.Switch
) -> None:
"""Initialize a BleBox switch feature."""
super().__init__(coordinator, feature)
if feature.name:
self._attr_name = feature.name

@property
def is_on(self) -> bool | None:
"""Return whether switch is on."""
Expand Down
Loading
Loading