From: job Date: Wed, 20 Apr 2022 10:46:20 +0000 (+0000) Subject: Add Concatenated JSON output in filemode (rpki-client -j -f *) X-Git-Url: http://artulab.com/gitweb/?a=commitdiff_plain;h=530399e88e82edbb50048a2cf5ff6bfb6578c7e0;p=openbsd Add Concatenated JSON output in filemode (rpki-client -j -f *) The schema is still work in progress. OK claudio@ --- diff --git a/usr.sbin/rpki-client/extern.h b/usr.sbin/rpki-client/extern.h index 129e9b9f91f..fea4bdfe99f 100644 --- a/usr.sbin/rpki-client/extern.h +++ b/usr.sbin/rpki-client/extern.h @@ -1,4 +1,4 @@ -/* $OpenBSD: extern.h,v 1.128 2022/04/19 13:52:24 claudio Exp $ */ +/* $OpenBSD: extern.h,v 1.129 2022/04/20 10:46:20 job Exp $ */ /* * Copyright (c) 2019 Kristaps Dzonsons * @@ -196,7 +196,7 @@ struct mft { char *ski; /* SKI */ char *crl; /* CRL file name */ unsigned char crlhash[SHA256_DIGEST_LENGTH]; - time_t valid_from; + time_t valid_since; time_t valid_until; size_t filesz; /* number of filenames */ unsigned int repoid; @@ -598,12 +598,13 @@ int x509_location(const char *, const char *, const char *, /* printers */ char *time2str(time_t); +void x509_print(const X509 *); void tal_print(const struct tal *); void cert_print(const struct cert *); void crl_print(const struct crl *); -void mft_print(const struct mft *); -void roa_print(const struct roa *); -void gbr_print(const struct gbr *); +void mft_print(const X509 *, const struct mft *); +void roa_print(const X509 *, const struct roa *); +void gbr_print(const X509 *, const struct gbr *); /* Output! */ diff --git a/usr.sbin/rpki-client/mft.c b/usr.sbin/rpki-client/mft.c index 4d8caf2ac73..565a441b0bb 100644 --- a/usr.sbin/rpki-client/mft.c +++ b/usr.sbin/rpki-client/mft.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mft.c,v 1.59 2022/04/19 18:52:36 tb Exp $ */ +/* $OpenBSD: mft.c,v 1.60 2022/04/20 10:46:20 job Exp $ */ /* * Copyright (c) 2019 Kristaps Dzonsons * @@ -86,7 +86,7 @@ mft_parse_time(const ASN1_GENERALIZEDTIME *from, return 0; } - if ((p->res->valid_from = timegm(&tm_from)) == -1 || + if ((p->res->valid_since = timegm(&tm_from)) == -1 || (p->res->valid_until = timegm(&tm_until)) == -1) errx(1, "%s: timegm failed", p->fn); diff --git a/usr.sbin/rpki-client/parser.c b/usr.sbin/rpki-client/parser.c index abaa54c12cd..7751bc1a3a9 100644 --- a/usr.sbin/rpki-client/parser.c +++ b/usr.sbin/rpki-client/parser.c @@ -1,4 +1,4 @@ -/* $OpenBSD: parser.c,v 1.69 2022/04/19 13:25:08 claudio Exp $ */ +/* $OpenBSD: parser.c,v 1.70 2022/04/20 10:46:20 job Exp $ */ /* * Copyright (c) 2019 Claudio Jeker * Copyright (c) 2019 Kristaps Dzonsons @@ -428,9 +428,9 @@ proc_parser_mft_post(char *file, struct mft *mft, const char *path) } /* check that now is not before from */ - if (now < mft->valid_from) { + if (now < mft->valid_since) { warnx("%s: mft not yet valid %s", file, - time2str(mft->valid_from)); + time2str(mft->valid_since)); mft->stale = 1; } /* check that now is not after until */ @@ -1038,8 +1038,12 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) enum rtype type; int is_ta = 0; - if (num++ > 0) - printf("--\n"); + if (num++ > 0) { + if (outformats & FORMAT_JSON) + printf("\n"); + else + printf("--\n"); + } if (strncmp(file, "rsync://", strlen("rsync://")) == 0) { file += strlen("rsync://"); @@ -1050,7 +1054,10 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) } } - printf("File: %s\n", file); + if (outformats & FORMAT_JSON) + printf("{\n\t\"file\": \"%s\",\n", file); + else + printf("File: %s\n", file); type = rtype_from_file_extension(file); @@ -1081,7 +1088,7 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) mft = mft_parse(&x509, file, buf, len); if (mft == NULL) break; - mft_print(mft); + mft_print(x509, mft); aia = mft->aia; aki = mft->aki; break; @@ -1089,7 +1096,7 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) roa = roa_parse(&x509, file, buf, len); if (roa == NULL) break; - roa_print(roa); + roa_print(x509, roa); aia = roa->aia; aki = roa->aki; break; @@ -1097,7 +1104,7 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) gbr = gbr_parse(&x509, file, buf, len); if (gbr == NULL) break; - gbr_print(gbr); + gbr_print(x509, gbr); aia = gbr->aia; aki = gbr->aki; break; @@ -1112,6 +1119,11 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) break; } + if (outformats & FORMAT_JSON) + printf("\t\"validation\": \""); + else + printf("Validation: "); + if (aia != NULL) { struct auth *a; struct crl *c; @@ -1126,25 +1138,34 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) c = get_crl(a); if (valid_x509(file, x509, a, c, 0)) - printf("Validation: OK\n"); + printf("OK"); else - printf("Validation: Failed\n"); + printf("Failed"); } else if (is_ta) { if ((tal = find_tal(cert)) != NULL) { cert = ta_parse(file, cert, tal->pkey, tal->pkeysz); - printf("TAL: %s\n", tal->descr); - tal = NULL; + if (cert != NULL) + printf("OK"); } else { cert_free(cert); cert = NULL; - printf("TAL: not found\n"); + printf("Failed"); } - if (cert != NULL) - printf("Validation: OK\n"); - else - printf("Validation: Failed\n"); } + if (is_ta) { + if (outformats & FORMAT_JSON) { + printf("\",\n\t\"tal\": \"%s\"\n", tal->descr); + printf("}"); + } else { + printf("\nTAL: %s\n", tal->descr); + } + tal = NULL; + } else if (outformats & FORMAT_JSON) + printf("\"\n}"); + else + printf("\n"); + X509_free(x509); cert_free(cert); crl_free(crl); diff --git a/usr.sbin/rpki-client/print.c b/usr.sbin/rpki-client/print.c index d1a54f4d524..89bb4bbc9ea 100644 --- a/usr.sbin/rpki-client/print.c +++ b/usr.sbin/rpki-client/print.c @@ -1,4 +1,4 @@ -/* $OpenBSD: print.c,v 1.7 2022/04/12 11:05:50 job Exp $ */ +/* $OpenBSD: print.c,v 1.8 2022/04/20 10:46:20 job Exp $ */ /* * Copyright (c) 2021 Claudio Jeker * Copyright (c) 2019 Kristaps Dzonsons @@ -73,7 +73,6 @@ tal_print(const struct tal *p) int rder_len; size_t i; - printf("Trust anchor name: %s\n", p->descr); der = p->pkey; pk = d2i_PUBKEY(NULL, &der, p->pkeysz); @@ -90,76 +89,177 @@ tal_print(const struct tal *p) errx(1, "EVP_Digest failed in %s", __func__); ski = hex_encode(md, SHA_DIGEST_LENGTH); - printf("Subject key identifier: %s\n", pretty_key_id(ski)); - - printf("Trust anchor locations:\n"); - for (i = 0; i < p->urisz; i++) - printf("%5zu: %s\n", i + 1, p->uri[i]); + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"tal\",\n"); + printf("\t\"name\": %s\n", p->descr); + printf("\t\"ski\": %s\n", pretty_key_id(ski)); + printf("\t\"trust_anchor_locations\": ["); + for (i = 0; i < p->urisz; i++) { + printf("\"%s\"", p->uri[i]); + if (i + 1 < p->urisz) + printf(", "); + } + printf("]\n"); + + } else { + printf("Trust anchor name: %s\n", p->descr); + printf("Subject key identifier: %s\n", pretty_key_id(ski)); + printf("Trust anchor locations:\n"); + for (i = 0; i < p->urisz; i++) + printf("%5zu: %s\n", i + 1, p->uri[i]); + } + EVP_PKEY_free(pk); free(rder); free(ski); } +void +x509_print(const X509 *x) +{ + const ASN1_INTEGER *xserial; + char *serial = NULL; + + xserial = X509_get0_serialNumber(x); + if (xserial == NULL) { + warnx("X509_get0_serialNumber failed in %s", __func__); + goto out; + } + + serial = x509_convert_seqnum(__func__, xserial); + if (serial == NULL) { + warnx("x509_convert_seqnum failed in %s", __func__); + goto out; + } + + if (outformats & FORMAT_JSON) { + printf("\t\"cert_serial\": \"%s\",\n", serial); + } else { + printf("Certificate serial: %s\n", serial); + } + + out: + free(serial); +} + void cert_print(const struct cert *p) { - size_t i; - char buf1[64], buf2[64]; - int sockt; - char tbuf[21]; + size_t i, j; + char buf1[64], buf2[64]; + int sockt; + char tbuf[21]; - printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); - if (p->aki != NULL) - printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); - if (p->aia != NULL) - printf("Authority info access: %s\n", p->aia); - if (p->mft != NULL) - printf("Manifest: %s\n", p->mft); - if (p->repo != NULL) - printf("caRepository: %s\n", p->repo); - if (p->notify != NULL) - printf("Notify URL: %s\n", p->notify); - if (p->pubkey != NULL) - printf("BGPsec P-256 ECDSA public key: %s\n", p->pubkey); strftime(tbuf, sizeof(tbuf), "%FT%TZ", gmtime(&p->expires)); - printf("Valid until: %s\n", tbuf); - printf("Subordinate Resources:\n"); + if (outformats & FORMAT_JSON) { + if (p->pubkey != NULL) + printf("\t\"type\": \"router_key\",\n"); + else + printf("\t\"type\": \"ca_cert\",\n"); + printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski)); + if (p->aki != NULL) + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + x509_print(p->x509); + if (p->aia != NULL) + printf("\t\"aia\": \"%s\",\n", p->aia); + if (p->mft != NULL) + printf("\t\"manifest\": \"%s\",\n", p->mft); + if (p->repo != NULL) + printf("\t\"carepository\": \"%s\",\n", p->repo); + if (p->notify != NULL) + printf("\t\"notify_url\": \"%s\",\n", p->notify); + if (p->pubkey != NULL) + printf("\t\"router_key\": \"%s\",\n", p->pubkey); + printf("\t\"valid_until\": %lld,\n", (long long)p->expires); + printf("\t\"subordinate_resources\": [\n"); + } else { + printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); + if (p->aki != NULL) + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + x509_print(p->x509); + if (p->aia != NULL) + printf("Authority info access: %s\n", p->aia); + if (p->mft != NULL) + printf("Manifest: %s\n", p->mft); + if (p->repo != NULL) + printf("caRepository: %s\n", p->repo); + if (p->notify != NULL) + printf("Notify URL: %s\n", p->notify); + if (p->pubkey != NULL) + printf("BGPsec P-256 ECDSA public key: %s\n", p->pubkey); + printf("Valid until: %s\n", tbuf); + printf("Subordinate Resources:\n"); + } - for (i = 0; i < p->asz; i++) + for (i = 0; i < p->asz; i++) { switch (p->as[i].type) { case CERT_AS_ID: - printf("%5zu: AS: %u\n", i + 1, p->as[i].id); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"asid\": %u }", p->as[i].id); + else + printf("%5zu: AS: %u", i + 1, p->as[i].id); break; case CERT_AS_INHERIT: - printf("%5zu: AS: inherit\n", i + 1); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"asid_inherit\": \"true\" }"); + else + printf("%5zu: AS: inherit", i + 1); break; case CERT_AS_RANGE: - printf("%5zu: AS: %u -- %u\n", i + 1, - p->as[i].range.min, p->as[i].range.max); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"asrange\": { \"min\": %u, " + "\"max\": %u }}", p->as[i].range.min, + p->as[i].range.max); + else + printf("%5zu: AS: %u -- %u", i + 1, + p->as[i].range.min, p->as[i].range.max); break; } + if (outformats & FORMAT_JSON && i + 1 < p->asz + p->ipsz) + printf(",\n"); + else + printf("\n"); + } - for (i = 0; i < p->ipsz; i++) - switch (p->ips[i].type) { + for (j = 0; j < p->ipsz; j++) { + switch (p->ips[j].type) { case CERT_IP_INHERIT: - printf("%5zu: IP: inherit\n", i + 1); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"ip_inherit\": \"true\" }"); + else + printf("%5zu: IP: inherit", i + j + 1); break; case CERT_IP_ADDR: - ip_addr_print(&p->ips[i].ip, - p->ips[i].afi, buf1, sizeof(buf1)); - printf("%5zu: IP: %s\n", i + 1, buf1); + ip_addr_print(&p->ips[j].ip, + p->ips[j].afi, buf1, sizeof(buf1)); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"ip_prefix\": \"%s\" }", buf1); + else + printf("%5zu: IP: %s", i + j + 1, buf1); break; case CERT_IP_RANGE: - sockt = (p->ips[i].afi == AFI_IPV4) ? + sockt = (p->ips[j].afi == AFI_IPV4) ? AF_INET : AF_INET6; - inet_ntop(sockt, p->ips[i].min, buf1, sizeof(buf1)); - inet_ntop(sockt, p->ips[i].max, buf2, sizeof(buf2)); - printf("%5zu: IP: %s -- %s\n", i + 1, buf1, buf2); + inet_ntop(sockt, p->ips[j].min, buf1, sizeof(buf1)); + inet_ntop(sockt, p->ips[j].max, buf2, sizeof(buf2)); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"ip_range\": { \"min\": \"%s\"" + ", \"max\": \"%s\" }}", buf1, buf2); + else + printf("%5zu: IP: %s -- %s", i + j + 1, buf1, + buf2); break; } + if (outformats & FORMAT_JSON && i + j + 1 < p->asz + p->ipsz) + printf(",\n"); + else + printf("\n"); + } + if (outformats & FORMAT_JSON) + printf("\t],\n"); } void @@ -172,81 +272,185 @@ crl_print(const struct crl *p) char *serial; time_t t; - printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"crl\",\n"); + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + } else + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); crlnum = X509_CRL_get_ext_d2i(p->x509_crl, NID_crl_number, NULL, NULL); serial = x509_convert_seqnum(__func__, crlnum); - if (serial != NULL) - printf("CRL Serial Number: %s\n", serial); + if (serial != NULL) { + if (outformats & FORMAT_JSON) + printf("\t\"crl_serial\": \"%s\",\n", serial); + else + printf("CRL Serial Number: %s\n", serial); + } free(serial); ASN1_INTEGER_free(crlnum); - printf("CRL valid since: %s\n", time2str(p->issued)); - printf("CRL valid until: %s\n", time2str(p->expires)); + if (outformats & FORMAT_JSON) { + printf("\t\"valid_since\": %lld,\n", (long long)p->issued); + printf("\t\"valid_until\": %lld,\n", (long long)p->expires); + printf("\t\"revoked_certs\": [\n"); + } else { + printf("CRL valid since: %s\n", time2str(p->issued)); + printf("CRL valid until: %s\n", time2str(p->expires)); + printf("Revoked Certificates:\n"); + } revlist = X509_CRL_get_REVOKED(p->x509_crl); for (i = 0; i < sk_X509_REVOKED_num(revlist); i++) { - if (i == 0) - printf("Revoked Certificates:\n"); rev = sk_X509_REVOKED_value(revlist, i); - serial = x509_convert_seqnum(__func__, X509_REVOKED_get0_serialNumber(rev)); x509_get_time(X509_REVOKED_get0_revocationDate(rev), &t); - if (serial != NULL) - printf(" Serial: %8s Revocation Date: %s\n", - serial, time2str(t)); + if (serial != NULL) { + if (outformats & FORMAT_JSON) { + printf("\t\t{ \"serial\": \"%s\"", serial); + printf(", \"date\": \"%s\" }", time2str(t)); + if (i + 1 < sk_X509_REVOKED_num(revlist)) + printf(","); + printf("\n"); + } else + printf(" Serial: %8s Revocation Date: %s" + "\n", serial, time2str(t)); + } free(serial); } - if (i == 0) + + if (outformats & FORMAT_JSON) + printf("\t],\n"); + else if (i == 0) printf("No Revoked Certificates\n"); } void -mft_print(const struct mft *p) +mft_print(const X509 *x, const struct mft *p) { size_t i; char *hash; - printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); - printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); - printf("Authority info access: %s\n", p->aia); - printf("Manifest Number: %s\n", p->seqnum); + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"manifest\",\n"); + printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski)); + x509_print(x); + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + printf("\t\"aia\": \"%s\",\n", p->aia); + printf("\t\"manifest_number\": \"%s\",\n", p->seqnum); + printf("\t\"valid_since\": %lld,\n", (long long)p->valid_since); + printf("\t\"valid_until\": %lld,\n", (long long)p->valid_until); + } else { + printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + x509_print(x); + printf("Authority info access: %s\n", p->aia); + printf("Manifest Number: %s\n", p->seqnum); + printf("Manifest valid since: %s\n", time2str(p->valid_since)); + printf("Manifest valid until: %s\n", time2str(p->valid_until)); + } + for (i = 0; i < p->filesz; i++) { + if (i == 0 && outformats & FORMAT_JSON) + printf("\t\"filesandhashes\": [\n"); + if (base64_encode(p->files[i].hash, sizeof(p->files[i].hash), &hash) == -1) errx(1, "base64_encode failure"); - printf("%5zu: %s\n", i + 1, p->files[i].file); - printf("\thash %s\n", hash); + + if (outformats & FORMAT_JSON) { + printf("\t\t{ \"filename\": \"%s\",", p->files[i].file); + printf(" \"hash\": \"%s\" }", hash); + if (i + 1 < p->filesz) + printf(","); + printf("\n"); + } else { + printf("%5zu: %s\n", i + 1, p->files[i].file); + printf("\thash %s\n", hash); + } + free(hash); } + + if (outformats & FORMAT_JSON) + printf("\t],\n"); + + } void -roa_print(const struct roa *p) +roa_print(const X509 *x, const struct roa *p) { char buf[128]; size_t i; - printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); - printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); - printf("Authority info access: %s\n", p->aia); - printf("ROA valid until: %s\n", time2str(p->expires)); + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"roa\",\n"); + printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski)); + x509_print(x); + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + printf("\t\"aia\": \"%s\",\n", p->aia); + printf("\t\"valid_until\": %lld,\n", (long long)p->expires); + } else { + printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); + x509_print(x); + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + printf("Authority info access: %s\n", p->aia); + printf("ROA valid until: %s\n", time2str(p->expires)); + printf("asID: %u\n", p->asid); + } - printf("asID: %u\n", p->asid); for (i = 0; i < p->ipsz; i++) { + if (i == 0 && outformats & FORMAT_JSON) + printf("\t\"vrps\": [\n"); + ip_addr_print(&p->ips[i].addr, p->ips[i].afi, buf, sizeof(buf)); - printf("%5zu: %s maxlen: %hhu\n", i + 1, - buf, p->ips[i].maxlength); + + if (outformats & FORMAT_JSON) { + printf("\t\t{ \"prefix\": \"%s\",", buf); + printf(" \"asid\": %u,", p->asid); + printf(" \"maxlen\": %hhu }", p->ips[i].maxlength); + if (i + 1 < p->ipsz) + printf(","); + printf("\n"); + } else + printf("%5zu: %s maxlen: %hhu\n", i + 1, buf, + p->ips[i].maxlength); } + + if (outformats & FORMAT_JSON) + printf("\t],\n"); } void -gbr_print(const struct gbr *p) +gbr_print(const X509 *x, const struct gbr *p) { - printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); - printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); - printf("Authority info access: %s\n", p->aia); - printf("vcard:\n%s", p->vcard); + size_t i; + + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"gbr\",\n"); + printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski)); + x509_print(x); + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + printf("\t\"aia\": \"%s\",\n", p->aia); + printf("\t\"vcard\": \""); + for (i = 0; i < strlen(p->vcard); i++) { + if (p->vcard[i] == '"') + printf("\\\""); + if (p->vcard[i] == '\r') + continue; + if (p->vcard[i] == '\n') + printf("\\r\\n"); + else + putchar(p->vcard[i]); + } + printf("\",\n"); + } else { + printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); + x509_print(x); + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + printf("Authority info access: %s\n", p->aia); + printf("vcard:\n%s", p->vcard); + } } diff --git a/usr.sbin/rpki-client/rpki-client.8 b/usr.sbin/rpki-client/rpki-client.8 index 4ff77286441..b03188a28d2 100644 --- a/usr.sbin/rpki-client/rpki-client.8 +++ b/usr.sbin/rpki-client/rpki-client.8 @@ -1,4 +1,4 @@ -.\" $OpenBSD: rpki-client.8,v 1.59 2022/04/12 12:54:09 jmc Exp $ +.\" $OpenBSD: rpki-client.8,v 1.60 2022/04/20 10:46:20 job Exp $ .\" .\" Copyright (c) 2019 Kristaps Dzonsons .\" @@ -14,7 +14,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: April 12 2022 $ +.Dd $Mdocdate: April 20 2022 $ .Dt RPKI-CLIENT 8 .Os .Sh NAME @@ -112,7 +112,11 @@ If .Ar file is an rsync:// URI, the corresponding file from the cache will be used. This option implies -.Fl n . +.Fl n , +and can be combined with +.Fl j +to emit a stream of +.Em Concatenated JSON . .It Fl j Create output in the file .Pa json