Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
envenom
is an elegant application configurator for the more civilized age.
envenom
is written with simplicity and type safety in mind. It allows
you to express your application configuration declaratively in a dataclass-like
format while providing your application with type information about each entry,
its nullability and default values.
envenom
is designed for modern usecases, allowing for pulling configuration from
environment variables or files for more sophisticated deployments on platforms
like Kubernetes - all in the spirit of 12factor.
An envenom
config class looks like a regular Python dataclass - because it is one.
The @envenom.config
decorator creates a new dataclass by converting the config fields
into their dataclass
equivalents providing the relevant default field parameters. All
config classes created that way are marked as frozen=True
- because the config should
not change mid-flight - and eq=True
.
This also means it's 100% compatible with dataclasses. You can:
InitVar
/__post_init__
method for delayed initialization of fieldsclassmethod
s, staticmethod
s, and propertiesenvenom
will automatically fetch the environment variable values to populate
dataclass fields (optionally running parsers so that fields are automatically
converted to desired types). This works out of the box with all types trivially
convertible from str
, like Enum
and UUID
, and with any object type that can be
instantiated easily from a single string (any function (str,) -> T
will work as a
parser).
If using a static type checker the type deduction system will correctly identify most
mistakes if you declare fields, parsers or default values with mismatched types. There
are certain exceptions, for example T
will always satisfy type bounds T | None
.
envenom
also offers reading variable contents from file by specifying an environment
variable with the suffix __FILE
which contains the path to a file with the respective
secret. This aims to facilitate a common deploy pattern where secrets are mounted as
files (especially prevalent with Kubernetes).
All interaction with the environment is case-sensitive - we'll convert everything to
uppercase, and since _
is a common separator within environment variable names we use
_
to replace any and all nonsensical characters, then use __
to separate namespaces.
Therefore a field "var"
in namespaces ("ns-1", "ns2")
will be mapped to
NS_1__NS2__VAR
.
envenom
isn'tenvenom
has a clearly defined scope limited to configuration management from the
application's point of view.
This means envenom
is only interested in converting the environment into application
configuration and does not care about how the environment gets populated in the first place.
Things that are out of scope for envenom
include, but are not limited to:
YAML
/JSON
/INI
etc.)envenom
envenom
python -m pip install envenom
Config classes are created with the envenom.config
class decorator. It behaves exactly
like dataclasses.dataclass
but allows to replace standard dataclasses.field
definitions with one of envenom
-specific configuration field types.
from envenom import config
@config()
class MainCfg:
...
envenom
field typesenvenom
offers four supported field types:
required
for configuration variables that have to be provided. If the value cannot
be found, envenom.errors.MissingConfiguration
will be raised.optional
for configuration variables that don't have to be provided. If the value cannot
be found, it will be set to None
.with_default
for configuration variables where a default value can be provided. If the
value cannot be found, it will be set to the default.with_default_factory
for configuration variables where a default value can be provided.
If the value cannot be found, it will call the default factory and set the value to the result.This example shows how to build a basic config structure using a database config
as an example. It is available in the envenom.examples.quickstart
runnable module.
from functools import cached_property
from uuid import UUID, uuid4
from envenom import (
config,
optional,
required,
subconfig,
with_default,
with_default_factory,
)
from envenom.parsers import bool_parser
@config(namespace=("myapp", "db"))
class DbCfg:
scheme: str = with_default(default="postgresql+psycopg://")
host: str = required()
port: int = with_default(int, default=5432)
database: str = required()
username: str | None = optional()
password: str | None = optional()
connection_timeout: int | None = optional(int)
sslmode_require: bool = with_default(bool_parser(), default=False)
@cached_property
def auth(self) -> str:
if not self.username and not self.password:
return ""
auth = ""
if self.username:
auth += self.username
if self.password:
auth += f":{self.password}"
if auth:
auth += "@"
return auth
@cached_property
def query_string(self) -> str:
query: dict[str, str] = {}
if self.connection_timeout:
query["timeout"] = str(self.connection_timeout)
if self.sslmode_require:
query["sslmode"] = "require"
if not query:
return ""
query_string = "&".join((f"{key}={value}" for key, value in query.items()))
return f"?{query_string}"
@cached_property
def connection_string(self) -> str:
return (
f"{self.scheme}{self.auth}{self.host}:{self.port}"
f"/{self.database}{self.query_string}"
)
@config(namespace="myapp")
class AppCfg:
worker_id: UUID = with_default_factory(UUID, default_factory=uuid4)
secret_key: str = required()
db: DbCfg = subconfig(DbCfg)
if __name__ == "__main__":
cfg = AppCfg()
print(f"cfg.worker_id ({type(cfg.worker_id)}): {repr(cfg.worker_id)}")
print(f"cfg.secret_key ({type(cfg.secret_key)}): {repr(cfg.secret_key)}")
print(f"cfg.db.host ({type(cfg.db.host)}): {repr(cfg.db.host)}")
print(f"cfg.db.port ({type(cfg.db.port)}): {repr(cfg.db.port)}")
print(f"cfg.db.database ({type(cfg.db.database)}): {repr(cfg.db.database)}")
print(f"cfg.db.username ({type(cfg.db.username)}): {repr(cfg.db.username)}")
print(f"cfg.db.password ({type(cfg.db.password)}): {repr(cfg.db.password)}")
print(f"cfg.db.connection_timeout ({type(cfg.db.connection_timeout)}): {repr(cfg.db.connection_timeout)}")
print(f"cfg.db.sslmode_require ({type(cfg.db.sslmode_require)}): {repr(cfg.db.sslmode_require)}")
print(f"cfg.db.connection_string ({type(cfg.db.connection_string)}): {repr(cfg.db.connection_string)}")
Run the example:
python -m envenom.examples.quickstart
Traceback (most recent call last):
...
raise MissingConfiguration(self.env_name)
envenom.errors.MissingConfiguration: 'MYAPP__SECRET_KEY'
Immediately throws an error, as soon as it encounters a required field.
Run the example again with the environment set:
MYAPP__SECRET_KEY='}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB' \
MYAPP__DB__HOST='postgres' \
MYAPP__DB__DATABASE='database-name' \
MYAPP__DB__USERNAME='user' \
MYAPP__DB__SSLMODE_REQUIRE='t' \
MYAPP__DB__CONNECTION_TIMEOUT='15' \
python -m envenom.examples.quickstart
cfg.worker_id (<class 'uuid.UUID'>): UUID('edf6c50a-37a4-42d4-a2d4-c1ee1f3975bc')
cfg.secret_key (<class 'str'>): '}uZ?uvJdKDM+$2[$dR)).n4q1SX!A$0u{(+D$PVB'
cfg.db.host (<class 'str'>): 'postgres'
cfg.db.port (<class 'int'>): 5432
cfg.db.database (<class 'str'>): 'database-name'
cfg.db.username (<class 'str'>): 'user'
cfg.db.password (<class 'NoneType'>): None
cfg.db.connection_timeout (<class 'int'>): 15
cfg.db.sslmode_require (<class 'bool'>): True
cfg.db.connection_string (<class 'str'>): 'postgresql+psycopg://user@postgres:5432/database-name?sslmode=require&timeout=15'
See the documentation for more examples of advanced usage and instructions for setting up a development environment.
FAQs
An elegant application configurator for the more civilized age
We found that envenom demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.