trivy os软件包扫描原理分析
具体可以基于之前的博客来做
基于trivy获取基础镜像
参数修改一下:
cliOpt.ListAllPkgs = true
结果中会带有如下格式的结果:
"Results":[{"Target":"192.168.1.94:443/test22/centos:7 (centos 7.9.2009)","Class":"os-pkgs","Type":"centos","Packages":[{"ID":"acl@2.2.51-15.el7.x86_64","Name":"acl","Version":"2.2.51","Release":"15.el7","Arch":"x86_64","SrcName":"acl","SrcVersion":"2.2.51","SrcRelease":"15.el7","Licenses":["GPLv2+"],"Maintainer":"CentOS","DependsOn":["glibc@2.17-317.el7.x86_64","libacl@2.2.51-15.el7.x86_64","libattr@2.4.46-13.el7.x86_64"],"Layer":{"DiffID":"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02"},"Type":"rpm"},{"ID":"audit-libs@2.8.5-4.el7.x86_64","Name":"audit-libs","Version":"2.8.5","Release":"4.el7","Arch":"x86_64","SrcName":"audit","SrcVersion":"2.8.5","SrcRelease":"4.el7","Licenses":["LGPLv2+"],"Maintainer":"CentOS","DependsOn":["glibc@2.17-317.el7.x86_64","libcap-ng@0.7.5-4.el7.x86_64"],"Layer":{"DiffID":"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02"},"Type":"rpm"},
......
其中的原理就是根据对应的软件包信息文件来读取。前面的调用路径与基于trivy获取基础镜像一致。都是通过analyzer.RegisterAnalyzer函数将自己注册进analyzers的map中。最后就可以去获取镜像的软件包列表。
os的软件包代码都在pkg/fanal/analyzer/pkg/中。这里面有三个目录apk、dpkg、rpm。它们分别对应于alpine、ubuntu(debian)、centos操作系统。
我们以ubuntu为例来分析。系统启动时,会将dpkg分析器注册进来。代码如下:
func init() {analyzer.RegisterAnalyzer(&dpkgAnalyzer{})
}
根据前面关于基础镜像的博客,我们知道,只有Required返回成功才会进行分析。所以我们先看这个函数的代码:
const (analyzerVersion = 3statusFile = "var/lib/dpkg/status"statusDir = "var/lib/dpkg/status.d/"infoDir = "var/lib/dpkg/info/"
)
......func (a dpkgAnalyzer) Required(filePath string, _ os.FileInfo) bool {dir, fileName := filepath.Split(filePath)if a.isListFile(dir, fileName) || filePath == statusFile {return true}if dir == statusDir {return true}return false
}
主要逻辑就是通过检查当前文件是否是var/lib/dpkg/status或者当前为目录的话,就判定是否是var/lib/dpkg/status.d。很明显,这里考虑了一个问题,镜像中的文件是占大多数的,所以先检查文件名是否相同,对性能会好点。匹配成功返回true。
如果成功,就会进入Analyze函数。源码如下:
func (a dpkgAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {scanner := bufio.NewScanner(input.Content)if a.isListFile(filepath.Split(input.FilePath)) {return a.parseDpkgInfoList(scanner)}return a.parseDpkgStatus(input.FilePath, scanner)
}
如果是文件,则调用parseDpkgInfoList函数去解析软件包,如果是目录,则调用parseDpkgStatus,具体代码我们往下看。
parseDpkgInfoList函数:
// parseDpkgStatus parses /var/lib/dpkg/info/*.list
func (a dpkgAnalyzer) parseDpkgInfoList(scanner *bufio.Scanner) (*analyzer.AnalysisResult, error) {var installedFiles []stringvar previous stringfor scanner.Scan() {//一行一行的读取current := scanner.Text()if current == "/." {continue}// Add the file if it is not directory.// e.g.// /usr/sbin// /usr/sbin/tarcat//// In the above case, we should take only /usr/sbin/tarcat since /usr/sbin is a directoryif !strings.HasPrefix(current, previous+"/") {//这里去除了目录信息,将所有文件都加入到切片中installedFiles = append(installedFiles, previous)}previous = current}// Add the last fileinstalledFiles = append(installedFiles, previous)if err := scanner.Err(); err != nil {return nil, xerrors.Errorf("scan error: %w", err)}return &analyzer.AnalysisResult{SystemInstalledFiles: installedFiles,}, nil
}
parseDpkgStatus函数:
// parseDpkgStatus parses /var/lib/dpkg/status or /var/lib/dpkg/status/*
//这里注释说明数据来源,我们以/var/lib/dpkg/status为例,来分析下面的代码,数据格式在下方有展示
func (a dpkgAnalyzer) parseDpkgStatus(filePath string, scanner *bufio.Scanner) (*analyzer.AnalysisResult, error) {var pkg *types.Packagepkgs := map[string]*types.Package{}//创建一个临时的package map,key为通过软件名和版本构成的IDpkgIDs := map[string]string{}//以软件名为key,ID为value的mapfor scanner.Scan() {line := strings.TrimSpace(scanner.Text())if line == "" {//软件包的信息以空行结束,如果遇到空行说明当前软件包的解析结束,跳过,为下一个解析做好准备continue}pkg = a.parseDpkgPkg(scanner)//重点在这个函数中,开始解析软件包if pkg != nil {pkgs[pkg.ID] = pkgpkgIDs[pkg.Name] = pkg.ID}}if err := scanner.Err(); err != nil {return nil, xerrors.Errorf("scan error: %w", err)}a.consolidateDependencies(pkgs, pkgIDs)//依赖处理return &analyzer.AnalysisResult{PackageInfos: []types.PackageInfo{{FilePath: filePath,Packages: lo.MapToSlice(pkgs, func(_ string, p *types.Package) types.Package {return *p}),//将结果格式化成切片返回},},}, nil
}
/var/lib/dpkg/status的部分内容
Package: accountsservice
Status: install ok installed
Priority: optional
Section: admin
Installed-Size: 452
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Architecture: amd64
Version: 0.6.55-0ubuntu12~20.04.5
Depends: dbus, libaccountsservice0 (= 0.6.55-0ubuntu12~20.04.5), libc6 (>= 2.4), libglib2.0-0 (>= 2.44), libpolkit-gobject-1-0 (>= 0.99)
Suggests: gnome-control-center
Conffiles:/etc/dbus-1/system.d/org.freedesktop.Accounts.conf 06247d62052029ead7d9ec1ef9457f42
Description: query and manipulate user account informationThe AccountService project provides a set of D-Businterfaces for querying and manipulating user accountinformation and an implementation of these interfaces,based on the useradd, usermod and userdel commands.
Homepage: https://www.freedesktop.org/wiki/Software/AccountsService/
Original-Maintainer: Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>Package: accountsservice-ubuntu-schemas
Status: install ok installed
Priority: optional
Section: gnome
Installed-Size: 44
Maintainer: Ubuntu Desktop Team <ubuntu-desktop@lists.ubuntu.com>
Architecture: all
Multi-Arch: foreign
Source: gsettings-ubuntu-touch-schemas
Version: 0.0.7+17.10.20170922-0ubuntu1
Replaces: accountsservice-ubuntu-touch-schemas (<= 0.0.1+14.04.20140130.1-0ubuntu1), ubuntu-system-settings (<= 0.1+14.04.20140130-0ubuntu1)
Depends: accountsservice
Breaks: accountsservice-ubuntu-touch-schemas (<= 0.0.1+14.04.20140130.1-0ubuntu1), ubuntu-system-settings (<= 0.1+14.04.20140130-0ubuntu1)
Description: AccountsService schemas for Ubuntuaccountsservice-ubuntu-schemas contains a collection of AccountsService vendorextension schemas used by various components of an Ubuntu environment.
Homepage: https://launchpad.net/gsettings-ubuntu-touch-schemasPackage: acl
Status: install ok installed
Priority: optional
Section: utils
Installed-Size: 192
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Architecture: amd64
Multi-Arch: foreign
Version: 2.2.53-6
Depends: libacl1 (= 2.2.53-6), libc6 (>= 2.14)
Description: access control list - utilitiesThis package contains the getfacl and setfacl utilities needed for
......
parseDpkgPkg函数:
func (a dpkgAnalyzer) parseDpkgPkg(scanner *bufio.Scanner) (pkg *types.Package) {var (name stringversion stringsourceName stringdependencies []stringisInstalled boolsourceVersion stringmaintainer string)isInstalled = truefor {line := strings.TrimSpace(scanner.Text())if line == "" {break}switch {case strings.HasPrefix(line, "Package: ")://对照上面的例子,这里就是软件名name = strings.TrimSpace(strings.TrimPrefix(line, "Package: "))case strings.HasPrefix(line, "Source: "):// Source line (Optional)// Gives the name of the source package// May also specifies a versionsrcCapture := dpkgSrcCaptureRegexp.FindAllStringSubmatch(line, -1)[0]md := map[string]string{}for i, n := range srcCapture {md[dpkgSrcCaptureRegexpNames[i]] = strings.TrimSpace(n)}sourceName = md["name"]if md["version"] != "" {sourceVersion = md["version"]}case strings.HasPrefix(line, "Version: ")://版本version = strings.TrimPrefix(line, "Version: ")case strings.HasPrefix(line, "Status: "):isInstalled = a.parseStatus(line)case strings.HasPrefix(line, "Depends: ")://依赖dependencies = a.parseDepends(line)case strings.HasPrefix(line, "Maintainer: ")://维护者maintainer = strings.TrimSpace(strings.TrimPrefix(line, "Maintainer: "))}if !scanner.Scan() {break}}if name == "" || version == "" || !isInstalled {return nil} else if !debVersion.Valid(version) {log.Logger.Warnf("Invalid Version Found : OS %s, Package %s, Version %s", "debian", name, version)return nil}pkg = &types.Package{ID: a.pkgID(name, version),Name: name,Version: version,DependsOn: dependencies, // Will be consolidated laterMaintainer: maintainer,}//将解析结果保存到pkg中,// Source version and names are computed from binary package names and versions// in dpkg.// Source package name:// https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/pkg-format.c#n338// Source package version:// https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/pkg-format.c#n355if sourceName == "" {sourceName = name}if sourceVersion == "" {sourceVersion = version}if !debVersion.Valid(sourceVersion) {log.Logger.Warnf("Invalid Version Found : OS %s, Package %s, Version %s", "debian", sourceName, sourceVersion)return pkg}pkg.SrcName = sourceNamepkg.SrcVersion = sourceVersionreturn pkg
}
然后调用AnalysisResult的Merge函数将PackageInfos合并,继而调用其Sort函数进行排序。然后将结果保存在缓存中,这里是本地缓存。最后在Scanner的ScanArtifact中通过调用s.driver.Scan将结果格式化成types.Results,这里的driver会是local scanner,具体代码如下:
// Scan scans the artifact and return results.
func (s Scanner) Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, ftypes.OS, error) {artifactDetail, err := s.applier.ApplyLayers(artifactKey, blobKeys)switch {case errors.Is(err, analyzer.ErrUnknownOS):log.Logger.Debug("OS is not detected.")// Packages may contain OS-independent binary information even though OS is not detected.if len(artifactDetail.Packages) != 0 {artifactDetail.OS = ftypes.OS{Family: "none"}}// If OS is not detected and repositories are detected, we'll try to use repositories as OS.if artifactDetail.Repository != nil {log.Logger.Debugf("Package repository: %s %s", artifactDetail.Repository.Family, artifactDetail.Repository.Release)log.Logger.Debugf("Assuming OS is %s %s.", artifactDetail.Repository.Family, artifactDetail.Repository.Release)artifactDetail.OS = ftypes.OS{Family: artifactDetail.Repository.Family,Name: artifactDetail.Repository.Release,}}case errors.Is(err, analyzer.ErrNoPkgsDetected):log.Logger.Warn("No OS package is detected. Make sure you haven't deleted any files that contain information about the installed packages.")log.Logger.Warn(`e.g. files under "/lib/apk/db/", "/var/lib/dpkg/" and "/var/lib/rpm"`)case err != nil:return nil, ftypes.OS{}, xerrors.Errorf("failed to apply layers: %w", err)}var eosl boolvar results, pkgResults types.Results// Fill OS packages and language-specific packagesif options.ListAllPackages {//这里就是我们刚开始说的那个标志,如果为true,进行整合if res := s.osPkgsToResult(target, artifactDetail, options); res != nil {pkgResults = append(pkgResults, *res)}pkgResults = append(pkgResults, s.langPkgsToResult(artifactDetail)...)}// Scan packages for vulnerabilitiesif options.Scanners.Enabled(types.VulnerabilityScanner) {var vulnResults types.ResultsvulnResults, eosl, err = s.scanVulnerabilities(target, artifactDetail, options)if err != nil {return nil, ftypes.OS{}, xerrors.Errorf("failed to detect vulnerabilities: %w", err)}artifactDetail.OS.Eosl = eosl// Merge package results into vulnerability resultsmergedResults := s.fillPkgsInVulns(pkgResults, vulnResults)results = append(results, mergedResults...)} else {// If vulnerability scanning is not enabled, it just adds package results.results = append(results, pkgResults...)}// Scan IaC config filesif ShouldScanMisconfigOrRbac(options.Scanners) {configResults := s.MisconfsToResults(artifactDetail.Misconfigurations)results = append(results, configResults...)}// Scan secretsif options.Scanners.Enabled(types.SecretScanner) {secretResults := s.secretsToResults(artifactDetail.Secrets)results = append(results, secretResults...)}// Scan licensesif options.Scanners.Enabled(types.LicenseScanner) {licenseResults := s.scanLicenses(artifactDetail, options.LicenseCategories)results = append(results, licenseResults...)}// Scan misconfigurations on container image configif options.ImageConfigScanners.Enabled(types.MisconfigScanner) {if im := artifactDetail.ImageConfig.Misconfiguration; im != nil {im.FilePath = target // Set the target name to the file path as container image config is not a real file.results = append(results, s.MisconfsToResults([]ftypes.Misconfiguration{*im})...)}}// Scan secrets on container image configif options.ImageConfigScanners.Enabled(types.SecretScanner) {if is := artifactDetail.ImageConfig.Secret; is != nil {is.FilePath = target // Set the target name to the file path as container image config is not a real file.results = append(results, s.secretsToResults([]ftypes.Secret{*is})...)}}// For WASM plugins and custom analyzersif len(artifactDetail.CustomResources) != 0 {results = append(results, types.Result{Class: types.ClassCustom,CustomResources: artifactDetail.CustomResources,})}for i := range results {// Fill vulnerability detailss.vulnClient.FillInfo(results[i].Vulnerabilities)}// Post scanningresults, err = post.Scan(ctx, results)if err != nil {return nil, ftypes.OS{}, xerrors.Errorf("post scan error: %w", err)}return results, artifactDetail.OS, nil
}
osPkgsToResult代码:
func (s Scanner) osPkgsToResult(target string, detail ftypes.ArtifactDetail, options types.ScanOptions) *types.Result {if len(detail.Packages) == 0 || !detail.OS.Detected() {return nil}pkgs := detail.Packagesif options.ScanRemovedPackages {pkgs = mergePkgs(pkgs, detail.ImageConfig.Packages)//主要是去重}sort.Sort(pkgs)return &types.Result{Target: fmt.Sprintf("%s (%s %s)", target, detail.OS.Family, detail.OS.Name),Class: types.ClassOSPkg,//标识为os的软件包Type: detail.OS.Family,//os namePackages: pkgs,}
}
至此,代码逻辑基本讲解完了。
相关文章:

trivy os软件包扫描原理分析
具体可以基于之前的博客来做 基于trivy获取基础镜像 参数修改一下: cliOpt.ListAllPkgs true 结果中会带有如下格式的结果: "Results":[{"Target":"192.168.1.94:443/test22/centos:7 (centos 7.9.2009)","Clas…...

算法训练营 day48 动态规划 完全背包 零钱兑换 II 组合总和 Ⅳ
算法训练营 day48 动态规划 完全背包 零钱兑换 II 组合总和 Ⅳ 完全背包 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物…...

Java 基础(1)—泛型简单使用
一、泛型定义及作用 泛型是一种编程机制,允许在编写代码时使用参数化类型,以在编译时实现类型安全。 以下是泛型作用: 增强代码可读性和可维护性:通过在代码中使用泛型参数,可以使代码更清晰、更具有可读性和可维护性…...

内存卡损坏了怎么恢复?
内存卡损坏了怎么恢复?现在我们身边有不少电子设备都是用存储卡来存储数据的。一旦需要方便我们直接导出使用。但这存储的数据也不是一定安全的,当我们将内存卡连接到电脑时,难免会碰到病毒、格式化等提示,在这些情况下,可能会导…...

Mysql使用规范(纯技术和实战建议)
1、事务隔级别: (强制):Repeatable-Read(重复读),且不能在会话操作时临时开启隔离级别。 注: Repeatable-Read(重复读)隔离级别解决不了幻读。 可用 show variables l…...

Netty源码解读-EventLoop(二)
一、简介 NioEventLoop的重要组成:Selector、线程、任务队列,他既会处理io事件,也会处理普通任务和定时任务. 1.下面是Selector,注意有两个哦后面会讲 2.下面的爷爷类提供的Thread变量,其实下面发excutor用的就是这个…...

OSI模型详解
今天,我们详解OSI(Open System Inter-connection Reference Model)模型,来看看工业物联网的网络互联和数据互通。 OSI模型 1984年,国际标准化组织(International Organization for Standardization&#…...

Share Creators完成500万美元融资,以工具化手段帮助企业从数字资产管理中解放
近日,总部位于旧金山湾区的初创公司Share Creators宣布完成了新一轮500万美元的融资,投资方为五源资本和福昕PDF。本轮融资主要用于扩大客户基础,并加速在美国、欧洲和亚洲的业务发展。近几年,企业内容及数字资产管理全球市场正在…...

几个Base64编码工具,也有蹊跷
起因 需求:对一段内容进行base64加密,然后通过url的get请求进行发送到后台,由于加密的内容比较少,base64串也不是很长,我认为此方案可行。 于是找了三个base64编码的在线工具,分别是: 平台1&…...

Python|每日一练|排序|递归|字符串|数组|动态规划|单选记录:以特殊格式处理连续增加的数字|正则表达式匹配|地下城游戏
1、以特殊格式处理连续增加的数字(排序) 贡献者:EricLao 给出一串数字, 程序要把数字按照这样的格式输出,把连续增加的数字用 [x-y] 的形式表示,只显示这一组顺序数字的首位两个数字,不连续增…...

Spring Cloud微服务网关Gateway组件
目录 网关简介 什么是Spring Cloud Gateway Spring Cloud Gateway 功能特征 核心概念 工作原理 Spring Cloud Gateway快速开始 环境搭建 集成Nacos 路由断言工厂(Route Predicate Factories)配置 自定义路由断言工厂 过滤器工厂( …...

cluster nodes(集群节点)
CLUSTER NODES 复制 自3.0.0起可用。 时间复杂度: O(N)其中N是 Cluster 节点的总数 Redis 集群中的每个节点都有其当前集群配置的视图,由已知节点的集合给出,我们与这些节点的连接状态,它们的标志&…...

【Android学习】下载jar慢和gradle慢的情况
目录 问题出现的原因 解决方法 解决Gradle下载问题:手动安装 解决jar包下载慢问题:更改下载源 问题出现的原因 国内访问谷歌被墙导致访问速度慢或者干脆无法下载 解决方法 解决Gradle下载问题:手动安装 访问官网Gradle | Release Candi…...

下一个排列-力扣31-java
一、题目描述整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。例如,arr [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地&…...

前端面试题
1.HTTP request报文结构是怎样的 1.首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF(换行符) 2.首行之后是若干行请求头,包括general-header,request-header或者entity-hea…...

jsp游戏门户网站系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 jsp 游戏门户网站系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql,使…...

Git与IDEA强强联合(HTTPS协议连接)
最近在写项目的时候,在台式机和笔记本之间频繁切换,竟然还是用qq传压缩包,我自己都感觉无语,有git这样强大的版本管理工具,我竟然没想起来。然后也没有相关的博文就想来更新一篇。 那么如何使idea和git强强联合呢&…...

leetcode 第二题:两数相加-C语言实现
题目地址 备注: 不要忘记最后一个进位的可能。可以使用typedef,来简化struct的书写 代码实现: #include<stdio.h> #include<stdlib.h>struct listNode {int val;struct listNode* next; };// 使用typedef typedef struct lis…...

【人工智能】PTP网络时钟服务器在智能驾驶里的重要性
【人工智能】PTP网络时钟服务器在智能驾驶里的重要性 【人工智能】PTP网络时钟服务器在智能驾驶里的重要性 一辆宣称具备L4/L5自动驾驶功能的车辆,如果多个激光雷达之间的时间同步不够精确?如果传感器感知数据通过以太网传输到智驾域控制器的延迟不可控…...
【蓝桥杯集训3】二分专题(3 / 5)
目录 二分模板 1460. 我在哪? - 二分答案 哈希表 1221. 四平方和 - 哈希表 / 二分 1、哈希表 2、二分 自定义排序 1227. 分巧克力 - 113. 特殊排序 - 二分模板 l r >> 1 —— 先 r mid 后 l mid1 —— 寻找左边界 —— 找大于某个数的最小值lr…...

在成都的哪个培训机构学习Java好呢?
自从小课06年进入成都这个IT培训市场以来,短短十几年,招过很多学员,也见证过很多机构的起起落落。心中有万分的感慨,总结下来有这几点分享给大家,在选择培训机构时能看清本质,找到适合自己靠谱的机构学Java…...

传输层重要协议之UDP协议和TCP协议详解
更多关于UDP协议和TCP协议请移步官网:https://www.rfc-editor.org/standards#ISUDP标准协议文档-RFC 768TCP标准协议文档-RFC 793UDP协议详解UDP协议的特点:无连接、不可靠传输、面向数据报和全双工。UDP协议报文结构:关于端口号:…...

BNB Greenfield 成存储赛道“新贵”,BNB 生态的野心与破局
“从BNB Beacon Chain,到BNB Chain,再到BNB Greenfield ,三位一体的 BNB 生态格局正式形成。 ”在今年的2月1日,币安发布了分布式存储链BNB Greenfield,根据白皮书信息,它的特别之处在于其不仅具备基于SP&a…...

【SQL开发实战技巧】系列(十六):时间类型操作(上):日、月、年、时、分、秒之差及时间间隔计算
系列文章目录 【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事 【SQL开发实战技巧】系列(二):简单单表查询 【SQL开发实战技巧】系列(三):SQL排序的那些事 【SQL开发实战技巧…...

JavaScript知识点总结
JavaScript 一、介绍: 1.JavaScript是一种专门在浏览器编译并执行的编程语言 2.JavaScript处理用户与浏览器之间请求问题 3.JavaScript采用【弱类型编程语言风格】对【面向对象思想】来进行实现的编程语言 二、弱类型编程语言风格 VS 强类型编程语言风格 …...

adb命令记录
一、获取系统版本 adb shell getprop ro.build.version.release 二、手机文件拉取到电脑 adb命令 : adb pull source_path dest_path 示例: adb pull /sdcard/Movies/app_layout.txt ./ 从手机拉取app_layout.txt文件到当前路径。 三、电脑文件推送…...

9.Docker Swarm
Docker Swarm 基本概念 Swarm是使用SwarmKit构建的 Docker 引擎内置(原生)的集群管理和编排工具。Docker Swarm是 Docker 官方三剑客项目之一,提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。 使用它…...

基于tensorflow keras DNN神经网络训练预测豆瓣中文影评差评好评 附完整代码 +数据
首先看视频:https://www.bilibili.com/video/BV1r84y1p7q3/?spm_id_from=333.999.0.0 附完整的代码数据 完整的代码项目: 主要代码: # 导入包 import csv import jieba import tensorflow as tf from tensorflow...

商城系统必备营销工具(五)——积分商城
做商城,流量必不可少,日活跃度也很重要。现在各大APP、网站、小程序和微商城,基本都在为了巩固流量做积分商城,虽然已经随处可见,但很多企业商家却并没有将积分商城运作起来,积分商城也没有人浏览兑换商品。…...

SpringBoot08:Shiro
什么是Shiro? 一个Java的安全(权限)框架,可以完成认证、授权、加密、会话管理、Web集成、缓存等 下载地址:Apache Shiro | Simple. Java. Security. 快速启动 先在官网找到入门案例:shiro/samples/quick…...