8 min de leitura

Os horários de auditoria dos agentes resistem a mudanças de relógio?

Os horários de auditoria dos agentes podem avançar ou se repetir. Saiba como números de sequência, UTC, deslocamentos, correções e verificações de integridade preservam uma linha do tempo defensável.

Os horários de auditoria dos agentes resistem a mudanças de relógio?

O horário do relógio de parede é uma evidência, não uma garantia de ordenação. Um agente pode fazer duas chamadas perfeitamente legítimas enquanto o relógio do host volta no tempo, repete uma hora durante o horário de verão ou é corrigido depois de sair de um longo período de suspensão. Se o visualizador de auditoria ordenar essas chamadas apenas pelo horário exibido, poderá contar uma história convincente, mas falsa.

Use um número de sequência para responder «qual registro este diário aceitou primeiro?». Use um registro de data e hora de parede para responder «qual horário do calendário o gravador informou?». São perguntas diferentes. As equipes têm problemas quando fingem que um único campo responde às duas.

Para agentes autônomos, essa distinção é importante. Um revisor talvez precise estabelecer se um comando SSH veio depois de uma aprovação, se uma chamada HTTP foi repetida ou se uma revogação ocorreu antes da próxima ação. A resposta precisa resistir a um relógio ruim no laptop e a uma mudança de horário inesperada, não apenas parecer organizada em uma tabela.

Os registros de data e hora não estabelecem uma ordem confiável dos eventos

Um registro de data e hora de parede não pode provar que um evento ocorreu antes de outro quando o relógio pode mudar. Ele apenas informa a leitura observada pelo relógio quando o software gravou o registro.

Considere esta sequência de uma máquina que estava dez minutos adiantada até que a sincronização de horário a corrigiu:

seq 841  2026-11-03T14:10:12.481Z  agent requested deploy status
seq 842  2026-11-03T14:00:13.107Z  agent requested deploy status
seq 843  2026-11-03T14:00:14.052Z  approval recorded

Uma ordenação por horário coloca 842 e 843 antes de 841. O diário aceitou 841 primeiro. Nenhuma das duas visões é um erro de digitação. Elas respondem a perguntas diferentes.

Isso importa mesmo quando os horários aumentam. Um relógio pode andar devagar, avançar de repente ou ser corrigido por um usuário. Dois registros com horários de calendário crescentes ainda podem estar mais distantes ou mais próximos do que seus valores sugerem. Um registro de data e hora fornece uma coordenada observada em uma escala de tempo civil. Ele não fornece uma medição contínua de duração.

A RFC 3339 deixa clara a parte relacionada ao armazenamento: os registros precisam de um deslocamento quando o horário local está envolvido, e o UTC expresso com Z remove a ambiguidade do deslocamento durante a troca de dados. Isso é uma boa prática, mas a RFC 3339 não transforma o relógio do host em uma testemunha infalível. Ela define uma notação, não a verdade.

Dê a todo fluxo de auditoria somente para anexação um seq monotonicamente crescente. Atribua-o no componente que serializa as gravações, não em cada processo do agente. Se dois processos de agente puderem alocar números localmente e enviar seus registros mais tarde, você terá criado duas ordens e chamado isso de uma só.

O número de sequência faz uma afirmação limitada, mas útil:

  • Dentro de um diário, um seq menor entrou primeiro.
  • Lacunas significam que o investigador precisa explicar registros ausentes, retidos ou excluídos intencionalmente.
  • Uma duplicata significa que o gravador, importador ou armazenamento falhou.
  • O valor, sozinho, não diz nada sobre um evento em outra máquina.

Esse último ponto é ignorado porque um número inteiro com aparência global parece ter autoridade. Não tem. Um número de sequência precisa de um namespace. journal_id=macbook-17, seq=841 é uma declaração com limites. seq=841 em uma planilha é um convite ao exagero.

O horário de verão repete leituras do relógio local

O horário de verão faz o relógio local repetir uma hora em muitas regiões. Durante a transição de outono, 01:15 ocorre uma vez com um deslocamento UTC e novamente com outro. Um registro que armazena apenas 2026-11-01 01:15:00 descartou a informação necessária para distingui-los.

Não tente corrigir isso depois adivinhando qual ocorrência o operador quis dizer. Capture contexto suficiente quando o evento for gravado:

{
  "journal_id": "build-mac-07",
  "seq": 842,
  "event_id": "01JXYZ...",
  "recorded_at_utc": "2026-11-01T08:15:00.000Z",
  "local_offset": "-07:00",
  "time_zone": "America/Los_Angeles",
  "event_type": "agent.http.requested"
}

Na ocorrência posterior, a mesma exibição do relógio local pode carregar local_offset: "-08:00" e um valor UTC uma hora mais tarde. O deslocamento numérico preserva o fato observado. O nome do fuso horário ajuda uma pessoa a explicar por que o deslocamento existia, mas não deve substituí-lo no registro.

As regras de fuso horário mudam. Governos já alteraram datas de início, datas de término e até a existência do horário de verão, sem muita preocupação com seu analisador de registros. Se você armazenar uma data local e o nome de um fuso e recalcular o deslocamento anos depois usando um banco de fusos mais recente, poderá exibir uma evidência histórica de forma diferente daquela registrada pela máquina na época. Preserve o valor UTC original e o deslocamento capturado. Use as regras atuais para exibição apenas quando o visualizador identificar claramente o resultado como uma conversão feita nos dias de hoje.

A transição de primavera produz um problema diferente. Alguns horários locais nunca acontecem. Um sistema que aceite um prazo local digitado por uma pessoa, como 02:30 durante uma hora que foi pulada, deve rejeitá-lo ou pedir uma resolução explícita. Movê-lo silenciosamente para 03:30 é uma decisão de produto disfarçada de aritmética de calendário.

Para registros de auditoria, o UTC deve ser canônico. Exiba o horário local quando isso ajudar o leitor, mas mostre o deslocamento no mesmo campo. 2026-11-01 01:15:00 -08:00 é menos simples que 01:15, mas também é uma evidência.

Uma correção manual deve criar um novo registro

As edições manuais mostram se uma trilha de auditoria mantém seu valor ou se transforma em um feed de atividades bem-apresentado. Se um administrador corrigir um horário no próprio registro, a observação original desaparece. O investigador deixa de saber se o relógio antigo estava errado, se o conteúdo do evento estava errado ou se alguém queria que o histórico parecesse diferente.

Mantenha o registro original imutável. Adicione um registro de correção que identifique o evento anterior, informe o campo em disputa, registre o valor corrigido proposto e explique sua base. A correção não apaga o primeiro registro. Ela acrescenta um fato posterior: alguém fez e justificou uma afirmação sobre o primeiro registro.

Um payload de correção pode ser pequeno e ainda cumprir sua função:

{
  "seq": 913,
  "event_type": "audit.timestamp_corrected",
  "corrects_event_id": "01JXYZ...",
  "original_recorded_at_utc": "2026-11-03T14:10:12.481Z",
  "asserted_occurred_at_utc": "2026-11-03T14:00:12.481Z",
  "basis": "host time service report and neighboring journal entries",
  "actor": "admin-identifier"
}

Use asserted_occurred_at_utc apenas quando houver evidência para o horário revisado. Não renomeie o recorded_at_utc original. Ele descreve a leitura do relógio do gravador e continua historicamente correto mesmo quando essa leitura era imprecisa.

Separe estas três ideias tanto no esquema quanto na linguagem:

  1. occurred_at é o horário em que uma ação aconteceu, se o agente puder estabelecê-lo.
  2. observed_at é o horário em que um coletor específico viu a ação.
  3. recorded_at é o horário em que o gravador de auditoria confirmou sua entrada.

Eles podem ser iguais. Muitas vezes são. Ainda assim, não merecem o mesmo nome de campo.

A recomendação popular de «simplesmente corrigir dados incorretos» vem de sistemas de relatórios, nos quais um número errado deve desaparecer do painel. Sistemas de auditoria têm outra função. Eles preservam o caminho entre a observação e a conclusão. Uma correção visível é menos conveniente para leitores ocasionais, mas impede que o sistema transforme incerteza em certeza.

Correções do horário de rede podem mover o relógio de parede

A sincronização de horário pela rede melhora a precisão do relógio, mas a própria correção pode tornar os registros surpreendentes. Um cliente pode ajustar gradualmente sua velocidade ou avançar o relógio de uma só vez quando o deslocamento é grande o bastante ou quando a política permite. Um usuário ajustando o relógio manualmente, uma máquina virtual retomando a execução e um relógio de firmware com um valor incorreto podem produzir o mesmo resultado visível: o horário do calendário muda entre duas entradas de auditoria.

A documentação de clock_gettime do POSIX distingue o relógio de tempo real do relógio monotônico. CLOCK_REALTIME acompanha o horário do calendário e pode ser configurado. CLOCK_MONOTONIC não tem uma origem útil no calendário, mas não é configurado por clock_settime; ele é o tipo certo de fonte para medir um intervalo dentro de um único sistema em execução.

Essa distinção produz uma regra prática de dados. Registre o horário de parede para o investigador. Registre uma amostra monotônica quando precisar analisar tempo decorrido, comportamento de timeout ou ordenação em torno de uma correção. Não serialize um valor monotônico como se fosse uma data. Seu valor absoluto só tem significado em relação ao boot e ao domínio de relógio correspondentes.

A RFC 8633, orientação da IETF para operações NTP, discute explicitamente grandes mudanças de horário e afirma que os operadores não devem ignorar cegamente o limite de pânico do NTP em inicializações a frio. A tentação habitual é tratar toda correção como uma tarefa administrativa inofensiva. Ela não é inofensiva quando o horário controla a validade de tokens, a retenção, a reconstrução de incidentes ou a detecção de repetição.

Seu sistema de auditoria deve informar anomalias de horário como fatos de auditoria. Não deve tentar escondê-las com uma regra de ordenação. Um evento útil pode conter o último valor do relógio de parede, o novo valor, a diferença estimada, a origem da mudança, se conhecida, e o processo que a detectou. Se o sistema operacional não expuser uma causa, diga isso. Uma explicação falsa é pior que uma explicação ausente.

Mantenha uma pequena tolerância para variações normais. Não crie um evento dramático de mudança de relógio porque valores adjacentes diferem alguns milissegundos em uma direção inesperada entre gravadores concorrentes. O seq serializado fornece a ordem. Marque uma descontinuidade quando o relógio de parede retroceder além da precisão declarada ou quando um salto para frente entrar em conflito com a atividade esperada e precisar de análise.

Os números de sequência precisam de um escopo definido

Mantenha o núcleo da auditoria local
O núcleo do cofre é executado dentro do aplicativo assinado da barra de menus do Mac, não em um daemon separado.

Um número de sequência só funciona dentro do registro que o atribuiu. Tratá-lo como uma ordem global depois que os registros saem desse diário produz conclusões erradas em sistemas distribuídos de agentes.

Suponha que um agente de programação em um Mac solicite uma ação de API e que um host de build remoto grave uma ação SSH. Cada host tem seu próprio diário:

build-mac-07  seq 842  14:00:13Z  HTTP action requested
build-host-2  seq  91  14:00:14Z  SSH action accepted
build-mac-07  seq 843  14:00:15Z  HTTP result received

Você pode dizer que 842 precede 843 em build-mac-07. Pode dizer que 91 foi registrado por build-host-2 no horário informado. Não pode provar, apenas com esses campos, se o host remoto aceitou a ação SSH antes ou depois da primeira solicitação HTTP em tempo real.

Para estabelecer relações entre sistemas, adicione um vínculo explícito. Um identificador de solicitação propagado do chamador ao receptor pode conectar o evento da solicitação ao evento de recebimento. Um recibo assinado pelo receptor pode fornecer evidências mais fortes. Um coletor central pode atribuir uma sequência de coleta quando recebe os registros, mas essa sequência prova a ordem de chegada ao coletor, não a ordem de ocorrência nas fontes.

Não exagere o que cada elemento prova:

  • Um ID de solicitação comprova correlação quando os dois lados o preservam. Ele não comprova o horário de entrega.
  • Uma sequência central de ingestão comprova a ordem de ingestão. O atraso da rede pode reordenar a chegada.
  • Um relógio sincronizado reduz a incerteza. Ele não a elimina.
  • Um relógio lógico distribuído pode expressar causalidade se todos os participantes o carregarem corretamente. Ele não fornece o horário de parede.

Para muitas trilhas de auditoria de agentes, não é necessário um grande sistema de ordenação distribuída. É preciso manter limites honestos. Tenha uma sequência local para cada diário confiável, inclua IDs de correlação nos limites das ações e mostre o diário de origem em toda exportação. Assim, o revisor consegue ver onde a evidência é forte e onde ela passa a ser uma inferência.

Armazene evidências temporais suficientes para explicar uma divergência

Um campo timestamp isolado não é um esquema de auditoria. É uma preferência de exibição que escapou para o armazenamento.

Use uma estrutura de registro que mantenha separadas a ordem, a hora do calendário, a identidade da fonte e os dados de integridade. Os nomes exatos dos campos são sua escolha, mas os conceitos devem sobreviver à exportação e à retenção:

{
  "journal_id": "build-mac-07",
  "seq": 842,
  "event_id": "01JXYZ...",
  "recorded_at_utc": "2026-11-03T14:00:13.107Z",
  "recorded_offset": "-08:00",
  "time_zone": "America/Los_Angeles",
  "monotonic_ns": 3982188001123,
  "boot_id": "boot-identifier",
  "event_type": "agent.http.requested",
  "correlation_id": "request-identifier",
  "actor_process": "process-identifier",
  "previous_hash": "hex-value",
  "record_hash": "hex-value"
}

boot_id evita um erro comum com valores monotônicos. Um contador monotônico pode reiniciar depois de um reboot, portanto 3982188001123 de um boot não pode ser comparado ao mesmo valor de outro sem contexto adicional. Armazene-o apenas se for usá-lo e documente sua unidade. Um campo chamado monotonic_time que deixa o leitor tentando descobrir se significa nanossegundos, milissegundos ou ciclos do sistema desperdiça o campo.

recorded_offset é o deslocamento em vigor quando o gravador registrou o evento. Ele não substitui o UTC. Permite que uma pessoa veja o contexto do horário local da fonte e que um formatador preserve a interpretação civil original. Inclua um fuso horário nomeado apenas se a plataforma puder fornecê-lo de maneira confiável. Um deslocamento fixo é suficiente para ordenação e reconstrução.

Os campos de hash precisam de regras igualmente claras. Calcule um hash do registro sobre uma representação canônica de todos os campos cuja alteração mudaria seu significado, incluindo seq, os horários, o tipo de evento, a identidade do agente e o resumo do payload. Não faça o hash de um bloco JSON formatado se diferentes serializadores puderem alterar espaços em branco, ordem das propriedades ou formatação dos números. Faça a canonização primeiro.

Uma cadeia de hashes detecta um registro alterado quando a verificação começa em uma âncora confiável e cada registro confirma o registro anterior. Ela não prova que o relógio da fonte estava correto. Não prova que um evento aconteceu fora da máquina. Ela comprova uma afirmação mais restrita, mas ainda importante: o histórico verificado preservou as relações criptográficas produzidas pelo gravador.

É por isso que uma cadeia e uma sequência devem andar juntas. A sequência fornece a ordem local. A cadeia torna visível uma reescrita posterior. O horário acrescenta contexto de calendário. Nenhum deles pode fazer o trabalho dos outros.

Investigue reversões de horário sem inventar uma história

Encaminhe credenciais sem expô-las
O Sallyport injeta credenciais bearer, básicas ou em cabeçalhos personalizados, enquanto o agente recebe apenas os resultados.

Quando uma sequência posterior tem um horário anterior, comece pelas evidências que você já possui. Não comece chamando isso de repetição, bug do agente ou adulteração.

Use esta breve sequência de investigação:

  1. Verifique a continuidade da sequência do diário e o resultado da verificação de integridade antes de interpretar os valores de horário.
  2. Compare os valores UTC anterior e posterior, os deslocamentos locais, os identificadores de boot e quaisquer amostras monotônicas.
  3. Verifique se o host atravessou uma transição de horário de verão, reiniciou, retomou a execução ou registrou uma correção do serviço de horário.
  4. Siga os IDs de correlação até os diários adjacentes, mas trate o horário entre hosts como uma estimativa, a menos que um recibo ou mecanismo de ordenação compartilhado o comprove.
  5. Adicione um registro de anotação se as evidências sustentarem uma correção ou uma conclusão de anomalia temporal.

Veja uma falha que aparece em revisões reais. Um agente solicita uma ação de API às 09:02:04, a máquina desperta, o horário de rede recua quatro minutos e o resultado da ação é registrado às 08:58:07. Um painel ordena cronologicamente e mostra o resultado antes da solicitação. Um operador conclui que o resultado foi repetido, bloqueia o agente e começa a trocar as credenciais.

A sequência e os valores monotônicos contam uma história mais simples. A solicitação é a sequência 117. O resultado é a sequência 118. Os dois têm o mesmo identificador de boot. O contador monotônico avança cerca de três segundos. O relógio de parede mudou entre as entradas. O resultado veio depois da solicitação nesse diário, e a exibição do calendário está incorreta durante a correção.

Essa conclusão ainda deixa perguntas úteis. Por que o relógio diferia quatro minutos? A ação dependia de uma credencial com expiração limitada por tempo? Outro sistema recebeu a solicitação antes da correção? A investigação deve responder a essas perguntas separadamente. Não deve tentar forçar uma ordem temporal limpa apenas porque o visualizador prefere uma.

Se uma verificação de integridade falhar, pare de tratar o diário como uma linha do tempo estabelecida. Preserve a exportação, registre a falha de verificação fora do diário afetado, se possível, e obtenha uma cópia nova por um caminho independente. Uma cadeia com falha não identifica a pessoa ou o processo que alterou os dados. Ela informa que as evidências já não sustentam a afirmação de um histórico sem modificações.

Uma trilha evidente contra adulteração precisa de verificação offline

Bloqueie ações no cofre
Secure Enclave e Touch ID protegem o cofre por hardware no macOS; bloqueado significa que toda ação é negada.

Um registro de auditoria tem pouco valor forense se sua única prova de integridade depender do serviço ativo que o produziu. Um revisor precisa exportar os registros, levá-los a outra máquina e verificar a cadeia sem usar o cofre nem perguntar ao agente que realizou as ações.

O Sallyport projeta seus diários Sessions e Activity a partir de um único registro de auditoria criptografado e encadeado por hashes, e sp audit verify pode verificar a cadeia offline sobre o texto cifrado, sem uma chave do cofre.

Esse design trata de um problema diferente da precisão do relógio. A verificação offline informa se o histórico criptografado continua internamente consistente segundo o design da auditoria. Ela não certifica o relógio de parede do host. Mantenha as duas afirmações separadas nos relatórios: «a cadeia de auditoria foi verificada» e «o horário do evento foi corroborado por uma fonte independente» são declarações úteis, e nenhuma implica a outra.

Os procedimentos de exportação devem preservar a identidade do diário, o intervalo de sequências, o resultado da verificação e a versão do software que realizou a verificação. Um CSV apenas com horário e texto da ação é um relatório, não uma evidência de auditoria. Ele perde os campos que permitiriam a alguém contestar ou confirmar o relatório mais tarde.

Construa o visualizador em torno das divergências, não apenas da ordenação ideal

Um bom visualizador de auditoria não esconde anomalias de relógio. Ele ordena por sequência do diário por padrão dentro de um único diário, mostra o UTC ao lado do horário local quando o usuário expande uma linha e marca uma reversão temporal como uma descontinuidade de horário, em vez de reorganizar o histórico.

Ofereça aos leitores diferentes visões, mas dê a cada uma um nome preciso. «Ordem do diário» significa ordem da sequência. «Horário de calendário informado» significa ordem dos registros de data e hora e pode colocar registros causalmente posteriores antes dos anteriores. «Ordem de chegada ao coletor» significa a ordem em que outro serviço recebeu os dados. Evite uma ordenação genérica por «horário», pois ela torna invisível uma escolha forense relevante.

A interface também deve mostrar o alcance de sua certeza. Quando os eventos vierem de diários diferentes, agrupe-os por origem ou mostre um rótulo claro da origem ao lado de cada número de sequência. Se um ID de solicitação conectar os registros, mostre essa relação como um vínculo no modelo de dados e na interface. Não desenhe uma linha contínua na linha do tempo entre hosts a menos que seu sistema possa defendê-la.

A primeira ação é simples: examine uma das exportações existentes em busca de um horário local sem deslocamento, de um número de sequência sem identidade do diário ou de um caminho de edição no próprio registro. Qualquer um desses elementos basta para criar uma linha do tempo de incidente enganosa. Corrigir isso agora custa menos do que explicá-lo depois que uma ação do agente se tornar evidência.

FAQ

Ainda é possível confiar nos registros de auditoria depois que o relógio do sistema muda?

Sim, se o registro tiver um número de sequência durável atribuído por um único caminho de gravação. Considere esse número como a ordem em que o sistema de auditoria aceitou os eventos, e o registro de data e hora como evidência de quando cada evento diz que ocorreu. Um retrocesso do relógio pode fazer os horários correrem para trás sem quebrar a ordem das sequências.

Por que tenho registros de data e hora locais duplicados durante o horário de verão?

O horário local sem um deslocamento é ambíguo durante a transição de outono para o horário de verão. Armazene UTC como valor canônico para exibição e consultas, mantenha o deslocamento numérico capturado no momento da gravação e mostre o fuso horário nomeado apenas como contexto adicional. Assim, dois registros que dizem 01:30 continuam distintos.

Devo editar um registro de auditoria quando o horário está errado?

Não substitua silenciosamente um registro de auditoria. Mantenha o evento original, grave um evento separado de correção ou anotação e registre quem fez a correção, por quê e quais evidências a sustentaram. Um histórico aparentemente limpo, mas reescrito, é pior do que um histórico incômodo que continua explicável.

Os números de sequência comprovam a ordem dos eventos em várias máquinas?

Um número de sequência ordena eventos apenas dentro do escopo que o atribuiu. Um processo, diário ou gravador de registros pode emitir uma sequência útil. Dois hosts separados ou dois fluxos independentes precisam de um coordenador compartilhado, de um vínculo causal ou de uma declaração explícita de que os valores de sequência não podem ser comparados.

Qual é a diferença entre um ajuste brusco e um ajuste gradual do NTP?

Um ajuste brusco do relógio é uma mudança imediata para frente ou para trás, geralmente após uma correção grande. Um ajuste gradual altera a velocidade do relógio ao longo do tempo, fazendo o horário exibido convergir aos poucos. Ambos podem tornar enganosa a duração inferida pelos horários de parede, embora um retrocesso brusco seja muito mais fácil de identificar em um registro.

Quais campos um evento de auditoria de agente deve incluir?

Armazene um registro UTC preciso, o deslocamento usado para a exibição local, um número de sequência, um identificador de evento estável e a origem que observou ou registrou o evento. Se a duração for importante, capture também um valor de tempo monotônico, mas nunca o apresente como uma data de calendário. Um registro útil informa ao investigador o que aconteceu, em que ordem local e quais evidências temporais estavam disponíveis.

Como investigar entradas de auditoria que parecem fora de ordem?

Comece verificando se a sequência permanece contínua e se a verificação de integridade da auditoria foi aprovada. Depois, examine a diferença entre os horários de parede adjacentes e determine se o host fez um ajuste brusco ou gradual, reiniciou ou atravessou uma transição de horário de verão. Não acuse um agente de repetir uma ação apenas porque um registro parece anterior, até descartar o comportamento normal do relógio.

Um registro de auditoria encadeado por hashes torna os horários precisos?

Não. Uma cadeia de hashes pode mostrar que uma sequência mantida foi alterada apenas se o mecanismo de verificação abranger os campos usados e um invasor não puder substituir todo o histórico e sua âncora de confiança. Ela não torna um relógio incorreto preciso nem estabelece sozinha o horário real de uma ação.

Devo armazenar os horários de auditoria em UTC ou no horário local?

UTC é o padrão mais seguro para armazenamento, assinaturas, comparações e respostas de API. O horário local ainda ajuda as pessoas a relacionar um evento a um dia de trabalho ou a uma chamada de incidente, mas deve incluir o deslocamento numérico. Armazenar apenas um horário de calendário local cria uma ambiguidade evitável.

Posso confiar apenas nos horários para ordenar as ações de um agente?

Mantenha os horários de parede porque as pessoas precisam do contexto do calendário, mas nunca os use como única regra de ordenação. Combine-os com uma sequência de gravação e, quando duração ou comportamento de timeout forem importantes, com uma medição monotônica. Essa pequena redundância evita uma grande quantidade de análises incorretas de incidentes.

Sallyport

O Sallyport executa chamadas de API e comandos SSH pelo seu agente de IA. As chaves ficam em um cofre local no seu Mac; você aprova cada execução e toda ação vai para um registro selado.

© 2026 Sallyport · Código aberto sob Apache-2.0 · Oleg Sotnikov